MediaCodec - Decoding

컴퓨터공부/안드로이드 2020. 5. 24. 13:09 Posted by 아는 개발자

0. 시작하기 전에 

 

MediaCodec을 사용하기 위해선 기본적인 비디오 영상 처리에 대한 지식이 필요하다. 영상처리도 깊이 들어가면 한도 끝도 없을 것 같은데 이 포스트에서는 MediaCodec 을 이용해 Decoding 할 때 반드시 알고 있어야 하는 지식 정도로만 간추려서 소개하려고 한다.

 

여기서 사용한 예제 코드는 grafika https://github.com/google/grafika 저장소의 MoviePlayer.java 코드에서 deprecated 된 부분만 바꿨다. 실제로 동작하는 코드를 확인하고 싶다면 여기서 프로젝트를 받아 실행해보면 될 것 같다

 

1. Definition

 

Decoding은 영상 파일이 가지고 있는 정보를 추출해내는 작업이다. 우리가 흔히 볼 수 있는 영상플레이어들은 모두 비디오 파일을 읽고 이 정보를 화면에 뿌리는 디코딩 작업을 거친다. 디코딩 작업에는 화면 프레임 정보 뿐만 아니라 비디오가 가지고 있는 음성 파일도 포함한다. 영상에서 긁어온 정보들을 컨트롤해서 화면에 뿌려주는게 영상플레이어의 역할이다. 안드로이드에서는 MediaCodec 라이브러리를 통해 영상과 음성에 대해서 디코딩을 할 수 있다.

 

2. Create

 

MediaCodec 라이브러리를 이용해서 디코딩 작업을 담당하는 객체를 생성할 수 있다. 아래 코드에서 createDecoderByType 함수가 Decoder를 생성하는 함수다. 생성 전에 수행하는 작업을 볼 필요가 있는데, 앞의 MediaExtractor 클래스가 하는 역할은 소스 파일에서 비디오의 Meta 정보를 가져오는 역할을 한다. 비디오가 가지고 있는 Meta 정보로는 비디오의 bitrate, width, size 등등을 가져올 수 있는데 디코딩 작업을 할때는 비디오의 현재 압축 방식인 MIME이 필요하다. 이 압축방식은 비디오의 확장자마다 다른데 거의 모든 영상을 mp4로 담고 있는 현재는 대부분 h264 방식을 따르고 있다. 정확히 이게 어떤 방식으로 압축되는지는 나도 잘 모른다. 아무튼 이 정보를 통해 비디오 디코더를 읽어올 수 있다.

 

extractor = new MediaExtractor();
extractor.setDataSource(mSourceFile.toString());
int trackIndex = selectTrack(extractor);
if (trackIndex < 0) {
	throw new RuntimeException("No video track found in " + mSourceFile);
}
extractor.selectTrack(trackIndex);

MediaFormat format = extractor.getTrackFormat(trackIndex);

// Create a MediaCodec decoder, and configure it with the MediaFormat from the
// extractor.  It's very important to use the format from the extractor because
// it contains a copy of the CSD-0/CSD-1 codec-specific data chunks.
String mime = format.getString(MediaFormat.KEY_MIME);
decoder = MediaCodec.createDecoderByType(mime);
decoder.configure(format, mOutputSurface, null, 0);
decoder.start();

 

decoder.configure 함수의 첫번째 인자로는 아까 추출한 압축 방식을 전달했고 두번째 인자로 mOutputSurface라는 값을 전달하고 있다. mOutputSurface는 decoder로 받은 정보를 화면에 뿌려줄 도화지 역할을 하는데 위와같이 configure함수에 두번째 인자로 넣으면 디코딩 된 정보를 자동으로 화면에 뿌려줄 수 있게 된다. 

 

2. Extract Data

 

다음으로 할 작업은 디코더를 이용해 실제로 비디오 파일에서 영상 정보를 추출하는 것이다. 아래 그림은 MediaCodec에 관한 구글 개발자 문서에서 가져온 것인데, 코덱의 일종인 디코더는 영상을 가져올때 크게 input 작업과 output 작업을 거친다. 초록색으로 칠해진 클라이언트는 비디오에서 정보를 읽는 작업이고, 작은 정사각형을 채워넣는 작업은 앞서 클라이언트에서 읽어온 정보를 디코더 버퍼에 읽어온 채워넣는 작업이다. 이것 모두 개발자가 해야하는 일이다. 이 작업도 아래의 그림처럼 크게 두가지 단계로 정리해볼 수 있을 것 같다.

 

2.1 Input Phase

 

비디오에서 읽어온 정보를 Input 버퍼에 채워넣는 일이다. 코드를 하나하나 살펴보자. dequeueInputBuffer 함수는 받아온 나중에 받아올 정보를 채워너 넣을 수 있는 공간을 할당받는 함수다. 리턴값으로 index를 주는데 이 index 값은 엄청 길다란 배열의 index 값으로 생각하면 된다. 이 index 값을 받아서 데이터를 쓸 위치를 넣을 수 있다.

 

다음 작업으로는 비디오에서 데이터를 읽어오는 작업이다. 여기서 사용된 extractor 변수는 앞서 생성 작업에서 선언한 변수와 동일하다. 객체 내에 읽은 부분에 대한 iterator가 포함되어 있어서 어디까지 읽었는지 정보를 담고 있다. 코드 맨 마지막에 advance 함수를 통해 읽을 위치를 변경하는 것이 가능하다.

 

마지막으로 extractor에서 뽑아온 정보를 Input 버퍼에 넣어야한다. 앞어 읽어온 sample data의 리턴 값에 따라서 Input 버퍼에 넣는 정보가 다른데 이 값이 마이너스인 경우에는 모든 데이터를 읽은 경우이기 때문에 Input Buffer에 플래그 값으로 END_OF_STREAM을 넣어준다. 그 외의 경우에는 유효한 데이터인 것으로 보고 Input Buffer에 넣고 플래그 값을 0으로 넣는다. 함수의 네번째 인자로 시간 정보 값을 주는데 이 값은 현재 읽어온 버퍼가 비디오에서 몇초대에 위치하고 있는지에 대한 정보다.

 

int inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);
if (inputBufIndex >= 0) {
    if (firstInputTimeNsec == -1) {
        firstInputTimeNsec = System.nanoTime();
    }

    ByteBuffer inputBuf = decoder.getInputBuffer(inputBufIndex);
    // Read the sample data into the ByteBuffer.  This neither respects nor
    // updates inputBuf's position, limit, etc.
    int chunkSize = extractor.readSampleData(inputBuf, 0);
    if (chunkSize < 0) {
        // End of stream -- send empty frame with EOS flag set.
        decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L,
                MediaCodec.BUFFER_FLAG_END_OF_STREAM);
        inputDone = true;
        if (VERBOSE) Log.d(TAG, "sent input EOS");
    } else {
        if (extractor.getSampleTrackIndex() != trackIndex) {
            Log.w(TAG, "WEIRD: got sample from track " +
                    extractor.getSampleTrackIndex() + ", expected " + trackIndex);
        }
        long presentationTimeUs = extractor.getSampleTime();
        decoder.queueInputBuffer(inputBufIndex, 0, chunkSize,
                presentationTimeUs, 0 /*flags*/);
        if (VERBOSE) {
            Log.d(TAG, "submitted frame " + inputChunk + " to dec, size=" +
                    chunkSize);
        }
        inputChunk++;
        extractor.advance();
    }
} else {
    if (VERBOSE) Log.d(TAG, "input buffer not available");
}

 

2.2 Output Phase

 

Output Phase에서는 방금전 Input Phase에서 넣어둔 input buffer 정보를 추출하는 일을 한다. 각 비즈니스 로직에 따라서 추출한 정보를 화면에 뿌려주기도 하고 아니면 새로운 비디오를 만드는 작업으로 사용할 수도 있을 것 같다. dequeueOutputBuffer는 아까 dequeueInputBuffer 함수에서 넣어둔 정보를 가져오는 역할을 한다. 첫번째 인자는 out 타입으로 받아온 정보를 저장하고 리턴 값으로는 현재 디코더의 상태 값을 나타낸다.

 

TRY_AGAIN_LAYER는 현재 읽을 수 있는 Input buffer가 없을 때 발생한다. Input Buffer를 분명히 넣어 줬는데도 이 플래그 값이 발생하는데 input buffer를 여러차례 넣고 나면 제대로 읽을 수 있게 된다.  FORMAT_CHANGED는 디코더의 output format에 변화가 생겼다는 뜻인데 디코딩 작업에서는 딱히 중요한 점이 없다.

 

mBufferInfo 에서 받아온 정보의 플래그 값을 보게 되는데 END_OF_STREAM 가 포함되어 있으면 버퍼는 마지막인 것이다. 전에 Input Phase에서 END_OF_STREAM 플래그를 넣었던 바로 그녀석이 맞다. 

int decoderStatus = decoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
    // no output available yet
    if (VERBOSE) Log.d(TAG, "no output from decoder available");
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
    MediaFormat newFormat = decoder.getOutputFormat();
    if (VERBOSE) Log.d(TAG, "decoder output format changed: " + newFormat);
} else if (decoderStatus < 0) {
    throw new RuntimeException(
            "unexpected result from decoder.dequeueOutputBuffer: " +
                    decoderStatus);
} else { // decoderStatus >= 0
    if (firstInputTimeNsec != 0) {
        // Log the delay from the first buffer of input to the first buffer
        // of output.
        long nowNsec = System.nanoTime();
        Log.d(TAG, "startup lag " + ((nowNsec-firstInputTimeNsec) / 1000000.0) + " ms");
        firstInputTimeNsec = 0;
    }
    boolean doLoop = false;
    if (VERBOSE) Log.d(TAG, "surface decoder given buffer " + decoderStatus +
            " (size=" + mBufferInfo.size + ")");
    if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
        if (VERBOSE) Log.d(TAG, "output EOS");
        if (mLoop) {
            doLoop = true;
        } else {
            outputDone = true;
        }
    }

    boolean doRender = (mBufferInfo.size != 0);

    // As soon as we call releaseOutputBuffer, the buffer will be forwarded
    // to SurfaceTexture to convert to a texture.  We can't control when it
    // appears on-screen, but we can manage the pace at which we release
    // the buffers.
    if (doRender && frameCallback != null) {
        frameCallback.preRender(mBufferInfo.presentationTimeUs);
    }
    decoder.releaseOutputBuffer(decoderStatus, doRender);
    if (doRender && frameCallback != null) {
        frameCallback.postRender();
    }

    if (doLoop) {
        Log.d(TAG, "Reached EOS, looping");
        extractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
        inputDone = false;
        decoder.flush();    // reset decoder state
        frameCallback.loopReset();
    }
}

 

doRender 변수를 결정하는 요인은 mBufferInfo.size 가 0보다 클 때 인데, 이 정보는 받아온 정보가 화면에 뿌려줄 영상 정보인지 아닌지를 의미한다. 그래서 이 값이 유효하다면 각 비즈니스 로직에 따라서 화면에 뿌려주거나 음성을 재생하면 된다. 아래 코드에서는 frameCallback 함수에서 읽어온 정보에서 시간 정보만 추출해 가져가고 있다. 경우에 따라선 비디오 디코딩 정보를 담고 있는 mOutputSurface를 OpenGL에 그려주어 인코더의 input에 넣어주기도 한다. CTS 테스트 코드를 보면 추출한 오디오 버퍼 정보를 바로 인코더에 넣는 것을 볼 수 있다.

 

읽어온 정보에 대한 처리가 끝나면 releaseOutputBuffer를 통해 이 정보에 대한 처리가 완료 됐음을 처리한다.

 

2.3 release 

 

EndOfStream에 도달해 디코딩 작업이 완료되면 사용한 Decoder를 반드시 릴리즈 시켜줘야한다. 이것은 release 함수로 가능하다.

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

MediaCodec - Decoding  (0) 2020.05.24
MediaCodec - Getting Started  (0) 2020.05.24
Navigator - Getting Started  (0) 2020.04.20
안드로이드 그림자(Shadow) 효과 넣기  (0) 2020.04.18
Kotlin - Coroutine  (0) 2020.04.15
Kotlin으로 깔끔한 Builder를 만들어보자  (0) 2020.04.14

MediaCodec - Getting Started

컴퓨터공부/안드로이드 2020. 5. 24. 10:19 Posted by 아는 개발자

0. FFmpeg - 한계

 

 

동영상과 관련된 작업을 처리하는 툴로 가장 유명한 것은 아마 FFmpeg 일 것이다. 이 라이브러리에서는 영상의 트랜스코딩(압축)을 지원할 뿐만 아니라 영상내 텍스트/이미지 삽입 또는 영상을 회전시키고 자를 수 있는 기능도 제공하며 실행도 대부분의 개발자들에게 익숙한 형태인 커맨드라인 딱 한줄만 입력하면돼 비디오에 대해서 잘 모르는 사람들도 쉽게 사용할 수 있다. 하지만 FFmpeg은 이 모든 작업들이 소프트웨어적으로 구현되어 있어 느리다는 단점이 있고 C, C++인 로우 레벨로 만들어진 빌드 파일을 JVM 위에 돌아가는 자바 언어단에서 별개의 도입하는 것은 꺼리낌이 있다. 개발 외적으로는 GPL 라이센스를 가지고 있어서 이 라이브러리의 수정사항을 공개해야한다는 법적 이슈가 있고 무엇보다 대중적으로 사용하는 h264 압축 방식을 사용하는 경우(mp4 파일을 생성하는 경우) 특허 이슈가 있다고 한다. 이 특허문제에 대해선 인터넷상에서 갑론을박이 많은데 가장 중요한 주체인 FFmpeg 공식 홈페이지에서도 "자기들은 변호사는 아니라 잘 모르겠다"고 답변하는 것으로 보아 자유롭게 사용하기에는 찝찝한 툴이다.

 

ffmpeg 공식 홈페이지 Patent issue에 대한 답변. "We do not know" 문구가 눈에 띈다.

 

1. MediaCodec - 어쩔 수 없이 써야하는 존재

 

 

플랫폼 개발자들도 이런 문제점을 인식해서인지 영상을 처리할 수 있는 고유의 라이브러리를 도입했는데 안드로이드의 경우 MediaCodec 라이브러리가 이에 해당한다. FFmpeg의 한계점을 극복하고자 도입한 라이브러리이기에 더이상 라이센스 문제도 없고 JVM위에서 동작하는 안드로이드에서 사용하기에 적합한 형태이며 하드웨어 가속을 지원해 소프트웨어적으로 돌아가는 FFmpeg보다 빠르다.

 

하지만 단점도 만만치 않다. FFmpeg은 트랜스코딩 뿐만 아니라 다양한 툴을 포함하고 있고 영상에 대해서 잘 몰라도 쉽게 사용할 수 있었다. 그러나 MediaCodec은 영상 정보를 추출하는 디코딩 작업과 바이너리 정보를 조합해 새로운 영상을 만드는 인코딩 작업만 제공할 뿐이며 원래 FFmpeg에 있었던 텍스트를 삽입하고 영상을 자르는 기능은 모두 스스로 만들어야 한다. 즉 이제는 디코딩/인코딩이 무엇인지, 영상 파일은 어떤식으로 이뤄져 있는지 그리고 텍스트를 삽입하고 영상을 자를 수 있는 그래픽의 기본 지식까지 겸비해야 한다는 뜻. 평소에 게임을 만들어본 사람이나 영상쪽에 관심있는 사람이 아니면 이쪽에 대해서 아마 잘 모를 것이다. 그리고 공부하려고 해도 진입장벽이 있는 부분이라 러닝 커브가 높다.

 

아쉽게도 단점은 이것 만이 아니다(ㅠㅠ). MediaCodec 라이브러리는 직접 하드웨어 장비와 연계된 부분이기 때문에 구글은 API만 뚫어주고 퀄컴, 삼성 LSI와 같은 칩 제조사에서 이 부분을 직접 구현했는데 이 부분이 칩(AP)에 따라서 다르다. 똑같은 갤럭시 스마트폰, 동일한 모델임에도 불구하고 국내에서 주로 사용하는 엑시노스 칩에서는 동작하는 반면 해외에서 사용하는 퀄컴 칩에서는 동작이 안될 수가 있다. 그리고 똑같은 안드로이드 버전이고 퀄컴칩을 사용하는데도 불구하고 사용하는 칩의 버전이 달라 갤럭시 노트8은 되고 갤럭시 S9은 되는 현상도 발생한다. 물론 이 경우는 코드를 잘못짠 것에 해당하기는 하나... 같은 플랫폼에서 똑같은 코드가 칩마다 다르게 동작할 수 있다는 점은 플랫폼 개발자로서 영 찝찝한 점이다. 안드로이드 버전별로 대응해왔던 것에서 이제는 국내용, 해외용도 모두 다 봐야 한다는 뜻이니까. 칩제조사와 플랫폼 벤더가 통합된 iOS 개발자들이 부러워지는 순간이다.

 

더 난감한 점은 게다가 이쪽 부분은 제조사에서 코드를 숨겨놔 에러가 발생해도 코드도 볼 수 없다는 사실이다... Logcat 메시지에서도 에러가 발생하면 알려주는 정보가 0x 로 시작하는 16진수의 플래그값 외에 알려주는게 더 없다. 스택오버플로우에라도 의지해볼 수 있다면 좋으련만 이상하게도 MediaCodec 관련 정보는 별로 없다. MediaCodec이 2012년도에 등장했는데도 아직까지 이렇게 정보가 많지 않다는 것을 보면 다들 MediaCodec으로 개발한 정보를 숨겨놓는건지 아니면 쓰려다가 지레 포기하고 외부 라이브러리를 사용한 것인지. MediaCodec을 이용한 오픈소스 프로젝트가 몇몇 있기는 한데 코드에서 정작 중요한 정보들은 byte code로 꽁꽁 숨겨놨다.(이럴거면 왜 공개했다고 한건지) 인터넷 상에서 정보를 찾기는 어렵고 개발하는데 난감하지만 대안이 없어 어쩔 수 없이 사용해야하는 라이브러리다.

 

 

2. MediaCodec - 개발 참고 자료

 

 

개발하기 어렵지만 그래도 참고할 만한 자료가 전혀 없는 것은 아니다. 단, 다른 라이브러리들처럼 친절한 문서 페이지는 기대하지 않는게 좋다.

 

 

2.1 CTS 코드

 

구글에서는 CTS (호환성 테스트) 검증에 사용한 코드를 공개하고 있다. 이 테스트 코드는 모든 제조사들이 출시하기 전에 PASS 해야하기 때문에 여기 코드들은 칩 디펜던시가 없이 모두 안정적으로 동작한다고 봐도 될 것 같다. https://bigflake.com/mediacodec/ 라는 사이트에서 MediaCodec과 관련된 CTS 테스트코드 주소와 테스트 목적에 대해서 짤막하게 소개해주고 있으니 여기서 구현하려는 것과 가장 가까운 테스트 코드를 참고하자. 테스트 코드를 보면 알겠지만 구글에서도 테스트 코드는 거지같이짜서  한눈에 보기가 쉽진 않다.

 

2.2 grafika 

 

구글에서 MediaCodec관련 문의가 하도 많이 들어와 만든 것인지 모르겠으나 MediaCodec 개발자로서는 한여름의 에어컨과도 같은 오픈소스다. https://github.com/google/grafika 여기에는 MediaCodec 라이브러리를 이용해 응용할 수 있는 무비 플레이어, 카메리 영상 처리, 비디오 트랜스코딩과 같은 다양한 예제를 담고 있다. README 페이지에 이 코드는 구글 공식 프로덕트가 아니고(그럼 구글 저장소에는 왜 있는건지?) 테스트를 제대로 하지 않아 안정적이지 않다고도 크게 써놔서 이 코드들이 모든 디바이스에서 동작할지는 확신 할 수 없지만, MediaCodec을 기본적으로 어떻게 써야할지 감을 익힐때 사용하면 유용하다. 

 

2013, 2014년도에 주로 작성되고 그 이후에는 최신 안드로이드버전 호환만 관리했기 때문에 모든 코드가 JAVA로 되어 있어 Kotlin으로 옮길 때 린트가 많이 생기는 단점이 있다.

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

MediaCodec - Decoding  (0) 2020.05.24
MediaCodec - Getting Started  (0) 2020.05.24
Navigator - Getting Started  (0) 2020.04.20
안드로이드 그림자(Shadow) 효과 넣기  (0) 2020.04.18
Kotlin - Coroutine  (0) 2020.04.15
Kotlin으로 깔끔한 Builder를 만들어보자  (0) 2020.04.14

Navigator - Getting Started

컴퓨터공부/안드로이드 2020. 4. 20. 23:21 Posted by 아는 개발자

안드로이드 Navigator 라이브러리는 프래그먼트를 이용해서 화면을 전환하는 작업을 돕는 라이브러리다. 로그인후 메인 화면으로 이동하거나 글 작성하는 UX의 경우 저장하는 작업 까지 여러 화면을 거치게 되는데 이런 경우 여러개의 액티비티를 쓰거나, 매번 FragmentManager를 이용해서 메인 뷰를 차지하고 있는 Fragment를 교체(replace)해줘야 했다. 하지만 Navigator 라이브러리를 사용하면 이런 화면 전환 과정을 XML 파일로 관리할 수 있고 시각화도 가능해서 유지 관리에 도움이 된다.

 

먼저 XML 파일로 표시하면 이렇고,

 

fragment_nav_graph.xml

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/fragment_navi_example"
    app:startDestination="@id/start_fragment">

    <fragment
        android:id="@+id/start_fragment"
        android:name="kwony.kotlin.navigate.StartFragment">
        <action
            android:id="@+id/action_first_fragment"
            app:destination="@id/first_fragment"
            app:exitAnim="@anim/fragment_close_exit"/>
    </fragment>

    <fragment
        android:id="@+id/first_fragment"
        android:name="kwony.kotlin.navigate.FirstFragment">
        <action
            android:id="@+id/action_second_fragment"
            app:destination="@id/second_fragment"
            app:enterAnim="@anim/slide_in_left"/>
    </fragment>

    <fragment
        android:id="@+id/second_fragment"
        android:name="kwony.kotlin.navigate.SecondFragment"/>
</navigation>

 

이 정보는 미리보기로 이렇게 표시된다.

 

 

XML파일을 쭉 훑어보고 난 후 사진을 보면 대강 감이 올텐데 먼저 가장 최상위 startDestination은 시작하는 프래그먼트의 이름이다. 위의 사진에서는 start_fragment 가 이 화면 구성의 시작점이 된다. start_frament에서 action 속성이 하나 있는데 여기서 destination 값은 first_fragment, 바로 앞 start_fragment에서 화살표로 가리키는 클래스다. 마찬가지로 first_fragment 에서도 action 속성이 하나 있는데 여기서의 destination은 second_fragment이고 사진상에서는 second_fragment를 화살표로 가리키고 있다. 이처럼 navigatior 에서는 fragment의 action 속성을 통해 어떤 fragment로 이동해야하는지 정해줄 수 있다. 

 

app:exitAnim 속성 값은 프래그먼트가 사라질 때 줄 애니메이션 효과다. 기본으로 등록되어 있는 것을 사용해도 되고 직접 커스텀해서 넣을 수도 있다. 반대로 enterAnim은 프래그먼트가 생겨날 때 줄 수 있는 효과다. 간단하게 XML 파일의 형태로 넣을 수 있어서 쉽다.

 

위에서 설명한 내용을 적용하려면 navigator를 Activity에 넣고 선언한 Fragment들은 action 속성값에 선언된대로 이동하도록 코드를 호출 해야한다. 먼저 Activity 작업에 대한 코드는 다음과 같다.

 

0. Activity

 

Class쪽 수정 없이 XML 파일에 이미 만든 navigation 리소스를 넣는 구문만 추가하면 된다. 아래 소스만 넣으면 처음에 StartFragment 를 클래스에서 생성하지 않아도 자동으로 FragmentContainerView가 잡고 있는 영역에 StartFragment가 추가된다. 

 

그리고 여기서 app:defaultNavHost="true" 로 선언했는데 이렇게 두면 시스템상의 백버튼 액션을 가로채서 이 Navigator에서 사용할 수 있다. 이 말은 즉 SecondFragment로 까지 이동한 상태에서 백버튼을 누르면 그 이전에 stack에 쌓여 있는 FirstFragment로 이동하고 다시 한 번 백버튼을 누르면 그전에 stack에 있는 StartFragment로 이동할 수 있다는 것이다. 프래그먼트가 화면에서 비중있는 역할을 하는 경우 필수적인 속성이 된다.

 

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="kwony.kotlin.di.activity.DaggerRootActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/fragment_nav_di_graph" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

1. StartFragment

 

StartFragment에서는 FirstFragment로 이동할 수 있는 작업이 있어야 하는데 임의로 TextView를 누르면 그 작업이 호출 되도록 했다. Click Listener 내부를 보면 findnavController().navigate 함수가 부르는데 여기의 인자가 XML에서 StartFragment 내부에 선언한 action 이다.

 

class StartFragment: DaggerFragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        fr_nav_tv.text = "StartFragment"

        fr_nav_tv.setOnClickListener {
            findNavController().navigate(R.id.action_first_fragment)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_nav_children, container, false)
    }
}

 

2. FirstFragment

 

StartFragment와 코드는 거의 흡사하고 차이가 있는 부분은 아까 선언한 action의 id 값을 바꿔주는 부분만 다르다.

 

class FirstFragment: DaggerFragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        fr_nav_tv.text = "FirstFragment"

        fr_nav_tv.setOnClickListener {
            findNavController().navigate(R.id.action_second_fragment)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_nav_children, container, false)
    }
}

 

이 포스트는 Navigator의 아주 기본적인 기능에 대해서만 소개한 것이라 아직 라이브러리의 장점을 모두 말하지 못했다. 숨겨진 기능을 확인해보고 싶으시다면 구글 문서를 참고하거나 이 카테고리의 다음 글을 기대해도 좋을 것 같다.

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

MediaCodec - Decoding  (0) 2020.05.24
MediaCodec - Getting Started  (0) 2020.05.24
Navigator - Getting Started  (0) 2020.04.20
안드로이드 그림자(Shadow) 효과 넣기  (0) 2020.04.18
Kotlin - Coroutine  (0) 2020.04.15
Kotlin으로 깔끔한 Builder를 만들어보자  (0) 2020.04.14

안드로이드에서 그림자 효과를 넣는 방법으로는 UI의 elevation 속성 값을 조정하는 것과 직접 그림자용 리소스 파일을 만드는 방법이 있다. 이번 포스트에서는 이 두가지의 사용 방법과 각각의 장단점을 소개해보려고 한다.

 

1. elevation 값 조정하기 

 

UI에 가장 쉽게 섀도우 효과를 입힐 수 있는 방법이다. 안드로이드 API21 부터 UI 뷰들에 elevation 이라는 속성값이 추가 됐는데 이 값을 넣으면 UI가 Z축으로 위로 튀어나와 그림자 효과를 줄 수 있게 된다. 

 

elevation 값을 조정해 그림자 효과를 준 경우.

코드는 다음과 같다.

 

<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <ImageView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_margin="50dp"
            android:elevation="20dp"
            android:background="@color/colorPrimary" />
    </FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

 

elevation 값을 조정해서 효과를 줄 때는 주의해야할 점이 두가지 있는데 첫번째는 elevation 값에 넣어준 수치 만큼 View 주변에 여백을 충분하게 주어야 한다는 것이다. elevation으로 만든 그림자는 View의 width/height 영역 밖에서 발생하기 때문에 이 부분의 여백을 주지 않으면 그림자 효과가 발생하지 않는다. 

 

미리보기 화면을 통해 확인해보면 FrameLayout 내부의 ImageView 주변에 여백이 있는 것을 확인 할 수 있다.

두번째로는 background값이 투명하면 안된다. 불투명한 값으로 셋팅을 해줘야한다. 왜 불투명한 background 값을 셋팅해줘야하는지는 아직 잘 모르겠다; 하지만 투명한 값으로 세팅하면 그림자 효과가 나타나지 않는다.

 

이 방법은 편하긴 하지만 API21 버전부터 사용할 수 있고 하단부에만 그림자 효과를 줄 수 있다는 단점이 있다. 상하좌우 모두 그림자 효과를 주어야 할 때는 사용 할 수 가 없다. 이런 경우에는 직접 리소스 파일로 그림자 효과를 만들어야 한다.

 

2. 그림자용 리소스 파일 만들기

 

선이나 사각형을 코드로 만들 때 사용했던 XML 파일을 이용해서 그림자 효과를 줄 수 있다. 설명에 앞서서 아래 예시 코드와 이 코드를 입힌 UI 결과물을 먼저 보자. 코드가 길지만 반복 구문이 많으니 대강 훓어보는 것을 추천한다

 

shadow_test.xml

 

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >

    <!-- Drop Shadow Stack -->
    <item>
        <shape>
            <padding
                android:bottom="2.5dp"
                android:left="2.5dp"
                android:right="2.5dp"
                android:top="2.5dp" />

            <solid android:color="#00CCCCCC" />
        </shape>
    </item>
    <item>
        <shape>
            <padding
                android:bottom="2.5dp"
                android:left="2.5dp"
                android:right="2.5dp"
                android:top="2.5dp" />

            <solid android:color="#06CCCCCC" />
        </shape>
    </item>
    <item>
        <shape>
            <padding
                android:bottom="2.5dp"
                android:left="2.5dp"
                android:right="2.5dp"
                android:top="2.5dp" />

            <solid android:color="#09CCCCCC" />
        </shape>
    </item>
    <item>
        <shape>
            <padding
                android:bottom="2.5dp"
                android:left="2.5dp"
                android:right="2.5dp"
                android:top="2.5dp" />

            <solid android:color="#0BCCCCCC" />
        </shape>
    </item>
    <item>
        <shape>
            <padding
                android:bottom="2.5dp"
                android:left="2.5dp"
                android:right="2.5dp"
                android:top="2.5dp" />

            <solid android:color="#0DCCCCCC" />
        </shape>
    </item>
    <item>
        <shape>
            <padding
                android:bottom="2.5dp"
                android:left="2.5dp"
                android:right="2.5dp"
                android:top="2.5dp" />

            <solid android:color="#10CCCCCC" />
        </shape>
    </item>
    <item>
        <shape>
            <padding
                android:bottom="2.5dp"
                android:left="2.5dp"
                android:right="2.5dp"
                android:top="2.5dp" />

            <solid android:color="#12CCCCCC" />
        </shape>
    </item>
    <item>
        <shape>
            <padding
                android:bottom="2.5dp"
                android:left="2.5dp"
                android:right="2.5dp"
                android:top="2.5dp" />

            <solid android:color="#15CCCCCC" />
        </shape>
    </item>
    <item>
        <shape>
            <padding
                android:bottom="2.5dp"
                android:left="2.5dp"
                android:right="2.5dp"
                android:top="2.5dp" />

            <solid android:color="#17CCCCCC" />
        </shape>
    </item>
    <item>
        <shape>
            <padding
                android:bottom="2.5dp"
                android:left="2.5dp"
                android:right="2.5dp"
                android:top="2.5dp" />

            <solid android:color="#1ACCCCCC" />
        </shape>
    </item>

    <!-- Background -->
    <item>
        <shape>
            <solid android:color="@android:color/white" />
        </shape>
    </item>

</layer-list>

 

ImageView에 위에서 만든 리소스를 background로 넣었다.

 

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="RtlSymmetry">

        <ImageView
            android:layout_width="150dp"
            android:layout_height="150dp"
            android:layout_gravity="center"
            android:background="@drawable/shadow_test"/>
    </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.constraintlayout.widget.ConstraintLayout>

 

이 코드를 이용해 그림자 효과를 입혀본 결과는 다음과 같다.

 

예제 코드를 사용해 그림자 효과를 입힌 결과

사진을 자세히 보면 흰 사각형 바깥쪽에 촘촘히 작은 선들이 있는 것을 볼 수가 있다. 이건 위 예제 코드에서 2.5dp 기준으로 각각 색깔이 다른 사각형을 넣었기 때문이다. 여러개의 작은 장면들을 조합해서 연속된 애니메이션 효과로 보이게 한 것 처럼 이 그라데이션 효과도 작은 사각형들을 합해서 그림자처럼 보이게 만든 효과다. 

 

이 방법은 약간의 노가다가 필요하긴 하지만 개발자가 그림자 효과를 자유자재로 커스텀이 가능하다는 장점이 있다. 어떤 부분에 좀더 강조를 세게 주고 싶다거나 좌측 상단, 우측 하단, 상화좌우 전체에 그림자 효과를 선택해서 줄 수 있다. 

 

elevation을 이용한 방법과 차이가 있다면 이 방법은 그림자 영역이 뷰의 영역에 포함되어 있다는 것이다. 아래 그림을 보면 왼쪽 그림의 보라색 사각형이 elevation을 이용해서 그림자 효과를 준 경우고 하얀색 사각형이 리소스를 이용해서 그림자 효과를 준 경우인데, 미리보기 상으로는 하얀색 사각형이 더 작아보이지만 두 ImageView의 가로 세로 너비 값은 오른쪽 그림에서도 알 수 있듯이 동일하다. 리소스를 사용하면 그림자 영역을 View 내부에서 사용하기 때문에 원래 생각했던 ImageView의 크기와 약간 차이가 발생 할 수 있다. 상황에 따라서 단점이 될 수도 있고 장점이 될 수 도 있는 기능이라 섣불리 판단 할 수는 없을 것 같다. 단 차이점은 유의해서 알고가는 것이 좋을 것 같다.

 

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

MediaCodec - Getting Started  (0) 2020.05.24
Navigator - Getting Started  (0) 2020.04.20
안드로이드 그림자(Shadow) 효과 넣기  (0) 2020.04.18
Kotlin - Coroutine  (0) 2020.04.15
Kotlin으로 깔끔한 Builder를 만들어보자  (0) 2020.04.14
Exoplayer2 사용하기  (0) 2020.04.12

Kotlin - Coroutine

컴퓨터공부/안드로이드 2020. 4. 15. 22:47 Posted by 아는 개발자

Kotlin의 Coroutine은 비동기 작업을 지원하는 "lightweight threads" 인 컴포넌트다. 이미 안드로이드에 있는 AsyncTask와 비슷한 역할을 수행하지만 Coroutine은 특별한 오버라이드 함 수 없이 간단하게 구현이 가능하고 깊게 들어가면 세부 동작 방식과 구현 철학은 다르다. 이번 포스트에서는 Kotlin의 Coroutine에 대해서 전반적인 소개와 사용 방법을 소개하려고 한다.

 

 

 

먼저 Coroutine은 하나의 task가 아니라, 여러 개 순서가 정해진 sub-tasks의 집합이다. Coroutine에서는 여러 개의 sub-task가 존재하는데 이들의 실행 순서는 보장된(guaranteed) 순서로 실행이 된다. 즉 코드 상에서는 얘네들이 sequential 하게 짜여져 있어도 코드를 어떻게 짜느냐에 따라서 실행 순서는 다를 수 있다. 이게 무슨 뚱딴지 같은 소리인가 싶을 수도 있지만 이건 asynchronous 작업을 좀더 유연하게 지원하기 위한 철학 정도로 이해하면 될 것 같다. 글로 보면 이해가 잘 되지 않을 수 있는데 예제 코드를 본다면 감이 어느정도 잡힐 것이다.

 

0. Quick Example

 

GlobalScope.launch {
    async { withContext(this.coroutineContext) { printLog("1") } }
    async { withContext(this.coroutineContext) { printLog("2") } }
    async { withContext(this.coroutineContext) { printLog("3") } }
}

 

안드로이드에서 Coroutine을 사용한 예제 코드다. 아직 Coroutine에 대해서 잘 모르더라도 느낌상으로는 로그를 찍는 함수 호출로 미뤄 볼 때 로그는 1 -> 2 -> 3 의 순서로 찍혀야 할 것 같다. 하지만 실제 출력 결과 매번 실행할 때마다 순서가 다르게 나온다.

 

2020-04-15 20:38:06.190 24595-24791/kwony.study D/CoroutineSample: 1
2020-04-15 20:38:06.191 24595-24790/kwony.study D/CoroutineSample: 3
2020-04-15 20:38:06.191 24595-24791/kwony.study D/CoroutineSample: 2

 

그런데 이번에는 로그 출력용 객체에 await 함수를 붙여주면

 

GlobalScope.launch {
    async { withContext(this.coroutineContext) { printLog("1") } }.await()
    async { withContext(this.coroutineContext) { printLog("2") } }.await()
    async { withContext(this.coroutineContext) { printLog("3") } }.await()
}

 

 이렇게 결과가 달라지고 값도 고정되게 나온다.

 

2020-04-15 20:40:12.687 25004-25078/kwony.study D/CoroutineSample: 1
2020-04-15 20:40:12.689 25004-25078/kwony.study D/CoroutineSample: 2
2020-04-15 20:40:12.689 25004-25078/kwony.study D/CoroutineSample: 3

 

Coroutine을 사용하면 작업의 순서를 요리조리 조정할 수 있다. 맛보기로 이정도면 좋을 것 같다.

 

1. Coroutine Builder 

 

Coroutine을 사용하려면 Coroutine 객체를 생성하는 작업을 먼저해야한다. Coroutine Builder는 Coroutine을 생성하기 위한 팩토리 함수다. Kotlin에서는 Coroutine을 생성하기위한 기본 빌더 함수를 제공한다.

 

runBlocking

 

현재 시작중인 thread를 block 시키고 Coroutine 작업을 시작하게 하는 Coroutine 빌더다. Main Thread에서 이 함수가 호출 됐으면 UI가 잠시 멈추고 coroutine 함수가 실행된다. 테스트를 위해 onCreate 함수에서 runBlocking Coroutine을 생성하고 3초간 딜레이를 주었다. 그 결과 화면이 초기화되는 시간이 3초간 딜레이되는 현상이 발생한다.

 

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    runBlocking {
        delay(3000)
    }

 

synchronous하게 처리해야하는 작업에 사용되는데 현재 실행중인 thread를 block시키는 문제가 꽤 골치 아프기 때문에 가능하면 쓰지 않을 것을 추천하는 문서도 있다. 정말로 필요한 경우가 아니면 가급적 사용하지 않는 것이 좋을 수도.

 

GlobalScope 

 

runBlocking 빌더와 달리 현재 실행중인 Thread를 block 시키지 않고 백그라운드에서 작업을 실행하는 Coroutine이다. 최상위 Coroutine을 생성 할 때 사용하는데 이 Coroutine은 애플리케이션과 동일한 생명주기를 갖게 된다. 이에 대한 자세한 내용은 CoroutineScope 에서 소개하는 것이 좋을 것 같다. 예제 코드를 한번 보자.

 

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    
    GlobalScope.launch {
        delay(3000)
        Log.d(coroutineSampleTag(), "World")
    }
    Log.d(coroutineSampleTag(), "Hello")
}

 

GlobalScope 으로 만들어진 Coroutine 에서는 3초간 딜레이를 주고 World를 출력하고 생성 다음에는 Hello를 출력하는 코드다. 실행 결과는 아래와 같다. Hello가 먼저 출력되고 Coroutine 내부의 World 문자열은 3초뒤에 출력되는 것을 확인 할 수 있다.

 

2020-04-15 21:21:45.224 30001-30001/kwony.study D/CoroutineSample: Hello
2020-04-15 21:21:48.234 30001-30069/kwony.study D/CoroutineSample: World

 

CoroutineScope(context: CoroutineContext)

 

Coroutine이 동작할 context를 지정해서 Coroutine을 만드는 함수다. 저기 안에 인자에 특정 CoroutineContext를 넣을 수 있는데, 이에 대해서 자세하게 설명하는 것보다는 어떤 Thread에서 동작할 것인지 지정하는 것 정도로만 보면 좋을 것 같다. 대표적으로 Dispatchers.Main과 Dispatchers.IO가 있다.

 

CoroutineScope(Dispatchers.Main).launch {}
CoroutineScope(Dispatchers.IO).launch {}

 

Dispatchers.Main으로 넣은 경우는 해당 Coroutine 작업이 MainThread 즉 UI를 관장하는 Thread에서 돌아가도록 선언한 것이고 Dispatchers.IO인 경우에는 Blocking IO 용 공유 쓰레드 풀에서 동작하도록 지정한 것이다. RxJava에서 subscribeOn 함수에 넣은 값들과 유사한 개념이다. 이 Builder 함수들도 GlobalScope 처럼 진행중인 Thread를 block 하지 않고 동작한다.

 

2. async { }

 

Coroutine 초기 예제에서도 확인 했던 것 처럼 Coroutine 작업들은 비동기적으로 처리하는 것이 가능하다. 비동기로 처리하려는 Coroutine들은 async { } 로 작업을 선언하면 된다.

 

CoroutineScope(Dispatchers.IO).launch {
    printLog("Coroutine Start")
    val deferred1 = async {
        delay(3000)
        printLog("1")
        return@async 1
    }
    val deferred2 = async {
        delay(3000)
        printLog("2")
        return@async 2
    }
    val deferred3 = async {
        delay(3000)
        printLog("3")
        return@async 3
    }
    printLog("total sum: " + (deferred1.await() + deferred2.await() + deferred3.await()))
}

 

예제에서 확장해서 async Coroutine에 딜레이 3초와 리턴값을 넣고 마지막에 각 Coroutine 결과의 총합을 출력하는 코드를 추가했다.

 

2020-04-15 22:18:29.090 3877-3990/? D/CoroutineSample: Coroutine Start
2020-04-15 22:18:32.108 3877-3996/kwony.study D/CoroutineSample: 2
2020-04-15 22:18:32.109 3877-3993/kwony.study D/CoroutineSample: 3
2020-04-15 22:18:32.112 3877-3991/kwony.study D/CoroutineSample: 1
2020-04-15 22:18:32.125 3877-3996/kwony.study D/CoroutineSample: total sum: 6

 

Coroutine 의 로그 출력 순서는 실행할 때마다 변경되는 반면 마지막에 Coroutine 반환 값의 합을 출력하는 부분은 항상 마지막에 출력되는데 이는 각 async 객체에 await() 함수를 사용해서 리턴값을 반환했기 때문이다. await() 함수는 async 함수가 모두 종료될 때 까지 구문 실행을 기다리도록 하는 함수다. 합을 출력하는 부분은 세개의 Coroutine의 연산결과를 모두 기다려야하기 때문에 제일 마지막에 실행 될 수 밖에 없다.

 

또 주목할 만한 점은 모든 Coroutine에 3초씩 딜레이를 주었는데 각 Coroutine의 로그 출력 시간은 3초씩 차이가 나지 않는다. 이는 각각의 Coroutine이 Sequential하게 동작하지 않고 Parallel하게 동작했기 때문이다. Coroutine의 로그 출력 순서가 바뀌는 이유도 Parallel 하게 동작했기 때문이다.

 

3. suspend fun method() = coroutineScope { }

 

Kotlin 함수 선언 앞에 suspend와 coroutineScope 으로 body를 만들어주면 Coroutine 에서 실행가능한 함수로 선언 해줄 수 있다. 아래 코드는 앞서 예제로 소개한 코드랑 동일한 로직으로 동작하며 출력되는 결과도 동일하다 (Coroutine 로그 출력 순서만 빼면)

 

private suspend fun coroutineMsg(msg: String, ret: Int): Int = coroutineScope {
    delay(3000)
    printLog(msg)
    return@coroutineScope ret
}

CoroutineScope(Dispatchers.IO).launch {
    printLog("Coroutine Start")
    val deferred1 = async { coroutineMsg("1", 1) }
    val deferred2 = async { coroutineMsg("2", 2) }
    val deferred3 = async { coroutineMsg("3", 3) }
    printLog("total sum: " + (deferred1.await() + deferred2.await() + deferred3.await()))
}
2020-04-15 22:42:07.182 7959-8043/kwony.study D/CoroutineSample: Coroutine Start
2020-04-15 22:42:10.197 7959-8045/kwony.study D/CoroutineSample: 1
2020-04-15 22:42:10.197 7959-8048/kwony.study D/CoroutineSample: 3
2020-04-15 22:42:10.198 7959-8043/kwony.study D/CoroutineSample: 2
2020-04-15 22:42:10.217 7959-8044/kwony.study D/CoroutineSample: total sum: 6

 

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

Navigator - Getting Started  (0) 2020.04.20
안드로이드 그림자(Shadow) 효과 넣기  (0) 2020.04.18
Kotlin - Coroutine  (0) 2020.04.15
Kotlin으로 깔끔한 Builder를 만들어보자  (0) 2020.04.14
Exoplayer2 사용하기  (0) 2020.04.12
FragmentManagers Android  (0) 2020.04.06

Kotlin에서 제공하는 apply 범위 함수를 이용해서 클래스 내부 속성 값을 간결하게 선언할 수 있지만 DSL(Domain Specific Language) 언어인 점을 응용하면 여러 클래스를 중첩한 클래스의 속성값에 대해서 더욱 간결하게 값을 설정 할 수 있다. 얼마나 간결한지 글로 길게 설명하는 것 보다는 간단한 예시로 보는게 좋을 것 같다.

 

Kotlin의 Builder 패턴을 사용하면 아래와 같이 선언된 data 클래스들을

 

data class Group(
    val name: String,
    val company: Company,
    val members: List<Member>
)

data class Company(
    var name: String = ""
)

data class Member(
    val name: String,
    val alias: String,
    val year: Int
)

 

이렇게 선언 하는 것이 가능하다.

 

val redVelvet = group {
    name { "레드벨벳" }
    company {
        name { "SM Entertainment" }
    }
    members {
        member {
            name { "슬기" }
            alias { "곰슬기" }
            year { 1994 }
        }

        member {
            name { "아이린" }
            alias { "얼굴 천재" }
            year { 1991 }
        }

        member {
            name { "웬디" }
            alias { "천사" }
            year { 1994 }
        }
    }
}

 

이렇게 간결하게 코드를 만들기 위해선 각각의 클래스에 대해서 Builder 클래스와 lambda 함수를 사용한 내부에 셋팅 함수를 선언해야한다. Member 함수부터 구현 방법을 살펴보자.

 

1. MemberBuilder

 

class MemberBuilder {
    private var name: String = ""
    private var alias: String = ""
    private var year: Int = 0

    fun name(lambda: () -> String) {
        name(lambda)
    }

    fun alias(lambda: () -> String) {
        alias(lambda)
    }

    fun year(lambda: () -> Int) {
        year(lambda)
    }

    fun build() = Member(name, alias, year)
}

 

MemberBuilder 클래스 내부에는 Member 데이터 클래스와 동일하나 name, alias, year 를 변수로 가지는데 보면 셋팅하는 함수들의 인자가 lambda로 선언되어 있고 바로 내부 변수를 초기화해준다는 점만 다르다. lambda가 포함된 함수는 아까 레드벨벳 초기화 코드에서 확인 할 수 있듯이 간단히 primitive 인자값을 전달하는 방식 만으로도 선언이 가능하다. build() 함수는 현재까지 초기화된 정보로 Member 클래스를 만드는 작업이다. 다른 곳에서 호출 받게 된다.

 

2. MemberListBuilder

 

class MemberListBuilder {
    private val employeeList = mutableListOf<Member>()

    fun member(lambda: MemberBuilder.() -> Unit) =
        employeeList.add(MemberBuilder().apply(lambda).build())

    fun build() = employeeList
}

 

Group의 데이터 클래스에 Member가 리스트 형태로 선언돼있기 때문에 Member의 개수는 1개 이상이 될 수 있다. 그래서 복수의 Member에 대해서 처리할 수 있는 MemberListBuilder가 이 부분을 담당한다. 레드벨벳 초기화 코드에서 member { ... } 로 호출한 부분은 바로 이 MemberListBuilder 클래스의 내부 함수를 호출한 것이다. 함수 내부를 보면 받아온 정보를 가지고 바로 MemberBuilder() 클래스 내부의 build() 함수를 통해 멤버를 생성하고 내부 배열 변수(employeeList)에 추가한다. build() 함수에서는 가지고 있는 배열 정보를 리턴한다.

 

3. CompanyBuilder

 

class CompanyBuilder {
    private var name = ""

    fun name(lambda: () -> String) {
        this.name = lambda()
    }

    fun build() = Company(name)
}

 

CompanyBuilder는 말단 노드라 MemberBuilder랑 생긴게 거의 비슷하다. 굳이 다시 한번 설명하지 않아도 될 것 같다. lambda 인자로 값을 받고 build() 에서 현재 클래스 값을 전달한다.

 

4. GroupBuilder 

 

class GroupBuilder {
    private var name = ""
    private var company = Company("")
    private val employees = mutableListOf<Member>()

    fun name(lambda: () -> String) {
        name = lambda()
    }

    fun company(lambda: CompanyBuilder.() -> Unit) {
        company = CompanyBuilder().apply(lambda).build()
    }

    fun members(lambda: MemberListBuilder.() -> Unit) =
        employees.addAll(MemberListBuilder().apply(lambda).build())

    fun build() = Group(name, company, employees)
}

 

GroupBuilder 클래스에는 지금까지 만들어왔던 builder를 포함하고 있다. name 함수에서는 Group의 이름을 정하고, company 함수에서는 CompanyBuilder를 통해서 company 속성값 정보를 셋팅한다. members 함수에서도 마찬가지로 MemberListBuilder 클래스를 통해 현재 입력된 모든 멤버의 정보를 입력한다. GroupBuilder() 또한 build() 함수를 호출해서 현재 Group 클래스를 최종적으로 반환한다. 

 

GroupBuilder 클래스의 build() 함수를 효출하는 부분은 따로 함수를 만들어야하는데 이렇게 만들면 된다.

 

fun group(lambda: GroupBuilder.() -> Unit): Group {
    return GroupBuilder().apply(lambda).build()
}

 

선언부에서 알 수 있듯이 가장 먼저 호출한 함수는 group {..} 이었다.

 

전체 코드는 다음과 같다.

 

Builder 패턴 구현 부분 코드

 

데이터 초기화 부분 코드

 

 

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

안드로이드 그림자(Shadow) 효과 넣기  (0) 2020.04.18
Kotlin - Coroutine  (0) 2020.04.15
Kotlin으로 깔끔한 Builder를 만들어보자  (0) 2020.04.14
Exoplayer2 사용하기  (0) 2020.04.12
FragmentManagers Android  (0) 2020.04.06
ViewModelProviders.of deprecated  (0) 2020.04.06

Exoplayer2 사용하기

컴퓨터공부/안드로이드 2020. 4. 12. 15:02 Posted by 아는 개발자

0. ExoPlayer란?

 

안드로이드에서 영상 재생을 위해 사용하는 플레이어로 기본 내장 라이브러리인 MediaPlayer가 있었는데 스트리밍 서비스가 주류를 이루면서 구글에서 DASH와 SmoothStreaming을 지원하는 ExoPlayer 라이브러리를 도입했다. 유튜브, 네이버 동영상 프레임들도 Exoplayer를 사용하고 있다고 하니 앞으로 안드로이드 동영상 플레이어는 ExoPlayer가 주류를 이룰 것 같은 예감이다. 아니면 이미 그런지도 모르겠고.

 

ExoPlayer는 MediaPlayer에서 이미 지원하는 기능에서 새로운 기능을 추가한 것이기 때문에 로컬/인터넷 동영상 파일 재생은 당연히 가능하고 Android Media Codec 기반으로 작업을 해서 Media Codec가 도입되기 시작한 안드로이드 기기 (API16 이상)에선 대부분 문제 없이 동작 한다. 물론 일부 기능은 더 높은 API 버전이 필요하긴 하지만 이는 거의 특수한 경우인 것 같다. 이번 포스트에서는 ExoPlayer를 사용하는 방법을 간단히 다룰 예정이다.

 

1. Components 

 

ExoPlayer: ExoPlayer의 라이브러리중 Renderer, 즉 화면에 뿌려주는 역할을 하는 컴포넌트다. ExoPlayer 인터페이스로 커스텀하게 만들 수 있으며 SimpleExoPlayer는 ExoPlayer에서 제공하는 컴포넌트다. 특별히 커스터마이즈 할 것이 아니면 이걸 그냥 가져다 쓰는게 좋다. 아래 함수를 통해 만들 수 있다.

 

val simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(requireContext(), trackSelector)

 

TrackSelector: SimpleExoPlayer 생성 과정에서 두번째 인자로 전달된 클래스는 영상의 Track 정보를 세팅하는 역할을 한다. 이 정보라면 예를 들면 선호하는 오디오 언어는 어떤 것인지, 비디오 사이즈는 무엇인지, 비디오 bitrate는 어떤 것으로 할지 등등 이런 것들을 말한다. 이것도 Renderer와 동일하게 따로 커스터마이즈 할 수 있긴 하나 특별한 이유가 없다면 라이브러리에서 기본으로 만들어 둔 것을 쓰는게 가장 좋다.

 

아래 코드는 TrackSelector를 만들 때 AdaptiveTrackSelection 팩토리를 사용한 예시다. AdaptiveTrackSelection 팩토리 클래스는 현재 bandwidth 정보를 이용해 현재 선택된 track에서 최상의 퀄리티를 제공하는 역할을 한다고 한다. 더 자세한 내용은 라이브러리 내부 주석을 살펴보는 것이 좋을 것 같다. Streaming 서비스를 한다면 이쪽 클래스를 주요하게 보게될 것 같다.

 

val bandwidthMeter = DefaultBandwidthMeter()
val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(bandwidthMeter)
val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory)

 

MediaSource: 영상에 출력할 미디어 정보를 가져오는 클래스다. ExtractorFactory 클래스를 통해 만드는데 이 클래스는 DataSource 클래스를 주입해서 만든다. 여기서 사용한 DefaultDataSource도 다른 라이브러리처럼 ExoPlayer에서 uri 형태로된 데이터를 읽어오기 위해 기본적으로 제공하는 라이브러리다. 특별한 형태의 DataSource 클래스를 사용하고 싶다면 커스터마이즈가 가능하다. 

 

val extractorFactory = ExtractorMediaSource.Factory(DefaultDataSourceFactory(context, Util.getUserAgent(context, context!!.applicationInfo.packageName)))
val mediaSource = extractorFactory.createMediaSource(Uri.parse(mediaPath))

 

Player: 영상 재생을 위해선 미디어를 읽어오는 작업뿐만 아니라 영상을 UI 상에 뿌려줄 수 있는 뷰어가 필요한데 ExoPlayer용 뷰어가 따로 있다. 아래 코드를 XML에 넣으면 된다. 재생바, 앞으로 당기기기 같은 기본적인 UI 기능도 지원한다.

 

<com.google.android.exoplayer2.ui.PlayerView
        android:id="@+id/fr_main_player"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#000"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

 

2. Play Video 

 

앞서 소개한 컴포넌트들을 하나의 코드로 조합하면 재생이 가능하다. 코드의 순서는 설명한 순서와 조금 다른데 이는 클래스 생성 후에 주입하기 위함이다. exoPlayer.prepare(mediaSource)는 영상 정보를 가져오는 작업이고 fr_main_player.player.playWhenReady는 준비되면 영상을 시작하는 함수다.

 

val mediaPath = "http://somewhere...."
val bandwidthMeter = DefaultBandwidthMeter()
val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(bandwidthMeter)
val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory)
val exoPlayer = ExoPlayerFactory.newSimpleInstance(requireContext(), trackSelector)
val extractorFactory = ExtractorMediaSource.Factory(
    DefaultDataSourceFactory(
        context,
        Util.getUserAgent(context, context!!.applicationInfo.packageName)
    )
)
val mediaSource = extractorFactory.createMediaSource(Uri.parse(mediaPath))

fr_main_player.player = exoPlayer
exoPlayer.prepare(mediaSource)
fr_main_player.player.playWhenReady = true

 

3. Extension 

 

Player.Listener: 영상 재생중 로딩에 실패하거나 Track 속성이 바뀌거나 혹은 영상 재생이 완료된 경우에 대해서 리스너를 등록해줄 수 있는데 이 경우들은 뷰어에 리스너를 등록해서 구현이 가능하다. 아래 코드를 통해 어떤 경우에 대해서 콜백 호출이 가능한지 확인 해볼 수 있다. 추가로 아래 코드에선 영상 재생이 완료된 경우 다시 재생하도록 구현했다.

 

fr_main_player.player.addListener(object: Player.EventListener{
    override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters?) {}
    override fun onSeekProcessed() {}
    override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {}
    override fun onPlayerError(error: ExoPlaybackException?) {}
    override fun onLoadingChanged(isLoading: Boolean) {}
    override fun onPositionDiscontinuity(reason: Int) {}
    override fun onRepeatModeChanged(repeatMode: Int) {}
    override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {}
    override fun onTimelineChanged(timeline: Timeline?, manifest: Any?, reason: Int) {}
    override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
        if (playbackState == Player.STATE_ENDED) {
            fr_main_player.player.seekTo(0)
            fr_main_player.player.playWhenReady = true
        }
    }
})

 

CacheDataSource: 인터넷으로 영상을 받는 경우 여러번 재생을 할 때 마다 동일한 데이터를 계속 인터넷으로 불러오게돼 데이터를 낭비할 수도 있는 문제가 있다. ExoPlayer에서는 이 문제점을 해결하고자 별도의 미디어 데이터 저장 공간으로 Cache를 뒀다. 이것도 다양하게 커스터마이즈 할 수 있으나 가장 기본적인 사용 방법은 아래 코드와 같다. 

 

ExtractorMediaSource.Factory 함수에서 호출 할 수 있도록 임의의 클래스를 DataSource.Factory의 인터페이스를 구현한 형태로 만든다. 리턴 값으로는 CacheDataSource가 되는데 여기서 생성자에서 캐시가 가져야할 정보를 입력하게 된다. 아래 코드 보면 캐시의 크기도 설정 할 수 있도 플래그를 넣을 수 있는 것도 확인 할 수 있다.

 

private class CacheDataSourceFactory internal constructor(
    private val context: Context,
    private val defaultDataSourceFactory: com.google.android.exoplayer2.upstream.DataSource.Factory,
    private val maxCacheSize: Long,
    private val maxFileSize: Long,
    private val url: String
) : com.google.android.exoplayer2.upstream.DataSource.Factory {
    override fun createDataSource(): com.google.android.exoplayer2.upstream.DataSource {
        val evictor = LeastRecentlyUsedCacheEvictor(maxCacheSize)
        val simpleCache = SimpleCache(File(context.cacheDir, "media"), evictor)
        return CacheDataSource(
            simpleCache,
            defaultDataSourceFactory.createDataSource(),
            FileDataSource(),
            CacheDataSink(simpleCache, maxFileSize),
            CacheDataSource.FLAG_BLOCK_ON_CACHE or CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
            null
        )
    }
}

 

위 코드를 이용한 호출부는 다음과 같다. 앞서 설명한 CacheDataSource 팩토리 클래스에서 두번째 인자로 DataSourceFactory를 넣었는데 아래 구현부 코드를 확인해보면 이전에 만든 DefaultDataSourceFactory 클래스를 넣는 것을 볼 수 있다. 외부 데이터를 불러오는 작업은 기존 데이터 클래스를 따라 간다는 뜻이다.

 

val extractorCacheFactory = ExtractorMediaSource.Factory(
    CacheDataSourceFactory(requireContext(), DefaultDataSourceFactory(
        context,
        Util.getUserAgent(context, context!!.applicationInfo.packageName)
    ), MAX_CACHE_SIZE, MIN_CACHE_SIZE, mediaPath)
)

val mediaSource = extractorCacheFactory.createMediaSource(Uri.parse(mediaPath))

 

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

Kotlin - Coroutine  (0) 2020.04.15
Kotlin으로 깔끔한 Builder를 만들어보자  (0) 2020.04.14
Exoplayer2 사용하기  (0) 2020.04.12
FragmentManagers Android  (0) 2020.04.06
ViewModelProviders.of deprecated  (0) 2020.04.06
Exoplayer에 stetho 적용하기  (0) 2020.03.16

FragmentManagers Android

컴퓨터공부/안드로이드 2020. 4. 6. 15:56 Posted by 아는 개발자

FragmentManager는 동적인 UI를 제공하기 위한 클래스인 Fragment를 관리하는 컨트롤러 역할을 한다. Manager라는 이름에서 예상 할 수 있듯이 FragmentManager를 사용하면 현재 UI에 Fragment를 추가할 수도 있고 있는 것을 교체할 수도 있으며 제거까지 가능하다. 호출하는 함수는 Activity인지, Fragment인지에 따라 다른데 일반적으로 Activity는 supportFragmentManager를 호출하게 되고, Fragment는 childFragmentManager 또는 parentFragmentManager를 통해 호출한다. 지금까지 개발 할 때는 각각의 차이를 확인하지 않고 '일단 동작부터 되도록' 에 주안점을 뒀는데 이번 포스트를 통해서 각각의 차이점과 적절한 쓰임새를 정리해보려고 한다.

 

0. 정의

 

FragmentManager의 경우에는 안드로이드 문서도 그닥 꼼꼼히 정리 되어있지 않고 스택오버플로우에서도 관심있게 다루는 주제가 아니라 참고할 만한 글이 별로 없었다. 그래서 지금까지 개발하면서 내가 나름대로 내린 사전적(?) 정의는 이렇다.

  • supportFragmentManager(SFM): Activity랑 상호작용하는(interacting) Fragment를 관리하는 클래스. Activity 클래스에서 호출이 가능하며 Activity 고유의 클래스다.

  • parentFragmentManager(PFM): 부모 UI 컴포넌트(Activity일 수도 있고 Fragment 일 수도 있다) 고유의 FragmentManager. Fragment 클래스에서 호출이 가능하다.

  • childFragmentManager(CFM): Fragment 고유의 FragmentManager 클래스. Fragment 별로 모두 다르다.

1. 그림 

 

SFM과 CFM과는 달리 PFM은 고유의 값이 아니라 특정 객체를 가르키는 값이다.  아래의 그림처럼 PartyActivity, PartyFragment 내부에 여러개의 PayFragment를 둔다고 해보자. 그러면 이들의 관계는 PartyActivity가 최상위 부모, PartyFragment는 부모, PayFragment는 자손이 되는 형태가 될 것이다. 

 

PartyActivity -> PartyFragment -> PayFragment

 

그러면 각각의 UI 요소들이 갖고 있는 FragmentManager는 아래와 같은 형태를 따르게 된다. 붉은 선으로 표현한 부분은 서로 동일한 객체인 것을 의미한다. Activity의 SFM을 이용해서 PartyFragment를 관리하고 있으므로 PFM는 부모인 PartyActivity 의 고유 FragmentManager, SFM을 가리키게 된다. 마찬가지로 PayFragment는 PartyFragment의 CFM으로부터 생성 됐으므로 PayFragment의 PFM은 부모인 PartyFragment의 CFM을 가리키게 된다.

 

PayFragment를 PartyFragment의 CFM을 이용해서 생성한 경우

 

로그를 통해 객체의 값을 확인해보면 위와 같은 구조를 가지는 것을 확인 할 수 있다. PartyFragment의 PFM 값은 PartyActivity의 SFM과 동일하고, PayFragment의 PFM값은 PartyFragment의 CFM과 동일하다.

 

UI 클래스 별로 PFM, SFM, CFM의 값을 출력한 결과

2. 주의점

 

PayFragment를 PartyFragment의 CFM으로 생성하지 않고 PFM으로 생성하는 경우 다음과 같이 그림이 달라진다. PartyFragment의 PFM은 PartyActivity의 SFM이며 PayFragment의 생성주체는 PartyActivity의 SFM이기 때문에, PayFragment의 PFM은 자연스레 PartyActivity의 SFM을 가리키게 된다.

 

PayFragment를 PartyFragment의 PFM으로 생성한 경우

 

물론 이런 형태여도 동작하는데는 큰 문제가 없을 것이다. 하지만 FragmentManager는 Fragment와 생성주기를 함께 하기 때문에, PartyFragment가 삭제되도 PayFragment는 SFM의 인스턴스로 남아있게 된다. 물론 이게 남아있는다고 동작상에 크게 흠을 주거나 메모리 릭을 유발하는 것은 아니지만 의도하지 않은 형태로 개발을 하다보면 정체모를 버그가 튀어나올 수 있으니 염두에두고 있는게 좋을 것 같다.

ViewModelProviders.of deprecated

컴퓨터공부/안드로이드 2020. 4. 6. 15:01 Posted by 아는 개발자

ViewModel을 주입할 때 주로 사용하는 ViewModelProviders 클래스는 lifecycle-extension 라이브러리가 2.2.0 버전업 되면서 통째로 Deprecated가 됐다. 하지만 ViewModelProvider(뒤에 s만 빠진 클래스가 맞다) 클래스를 통해 동일한 기능을 수행하도록 할 수 있다.

 

BEFORE

@Module(includes = [BaseActivityModule::class])
abstract class MainActivityModule {
    @Binds
    abstract fun provideActivity(activity: MainActivity): FragmentActivity

    @Module
    companion object {
        @Provides
        @JvmStatic
        fun provideViewModel(activity: FragmentActivity, viewModelFactory: ViewModelFactory): MainViewModel
                = ViewModelProviders.of(activity).get(MainViewModel::class.java)
    }
}

AFTER

@Module(includes = [BaseActivityModule::class])
abstract class MainActivityModule {
    @Binds
    abstract fun provideActivity(activity: MainActivity): FragmentActivity

    @Module
    companion object {
        @Provides
        @JvmStatic
        fun provideViewModel(activity: FragmentActivity, viewModelFactory: ViewModelFactory): MainViewModel
                = ViewModelProvider(activity, viewModelFactory).get(MainViewModel::class.java)
    }
}

 

fragment의 부모 activity 를 넘겨서 fragment와 activity가 동일한 viewmodel을 바라보게하는 기능도 정상적으로 동작한다

 

@Module(includes = [BaseFragmentModule::class])
abstract class PayFragmentModule {
    @Binds
    abstract fun provideFragment(fragment: PayFragment): Fragment

    @Module
    companion object {
        @Provides
        @JvmStatic
        fun provideViewModel(fragment: Fragment, viewModelFactory: ViewModelFactory): PartyViewModel 
                = ViewModelProvider(fragment.requireActivity(), viewModelFactory).get(PartyViewModel::class.java)
    }
}

 

함수단위면 몰라도 클래스 하나를 통째로 Deprecated 하는 것은 흔치 않는 일인 것 같은데 기존에 있는 ViewModelProvider 클래스에만 집중해서 개선하기 위함이지 않을까 조심스럽게 추측해본다. 그리고 기존에 있던 ViewModelProviders 코드도 대부분 ViewModelProvider를 호출하는 형태였기 때문에 둘이 겹치는 점도 많았던 것 같고. 

Exoplayer에 stetho 적용하기

컴퓨터공부/안드로이드 2020. 3. 16. 10:54 Posted by 아는 개발자

0. 소개

 

Exoplayer는 안드로이드 영상 재생플레이어로 많이 사용되는 오픈소스 프로젝트다. 요즘처럼 스트리밍으로 조각된 영상을 받는 경우엔 클라이언트의 재생 플레이어에서도 서버로 여러번 영상에 대한 요청을 보내게 되는데 이때 안드로이드 네트워크 인스펙터인 stetho를 사용하면 Exoplayer에서 보낸 요청들을 볼 수 있어서 디버깅 할 때 편리하다.

 

implementation 'com.google.android.exoplayer:extension-okhttp:2.7.0'

 

1. Exoplayer + Stetho

 

build.gradle 파일에 exoplayer extension 라이브러리를 추가한다. 감사하게도 exoplayer에서 stetho를 이용해 디버깅을 할 수 있도록 사전 작업을 해두었다.

 

 

라이브러리를 추가한 다음에는 안드로이드 Exoplayer 코드에서 http 요청 부분을 아래의 코드로 변경한다. OkHttpDataSourceFacotry는 DefaultHttpDataSourceFactory와 거의 동일해 동작에는 큰 차이가 없다고 봐도 무방하다.

 

+//    private val mMediaDataSourceFactory = DefaultDataSourceFactory(context, bandwidthMeter,
+//            DefaultHttpDataSourceFactory(Util.getUserAgent(context, context.applicationInfo.packageName), bandwidthMeter))
+
+    private val mMediaDataSourceFactory = DefaultDataSourceFactory(context, bandwidthMeter,
+            OkHttpDataSourceFactory(OkHttpClient.Builder().addNetworkInterceptor(StethoInterceptor()).build(), Util.getUserAgent(context, context.applicationInfo.packageName), bandwidthMeter))

 

2. Stetho

 

만약 애플리케이션에 StethoInterceptor를 사용하지 않고 있었다면 아래의 작업을 추가해야한다. build.gradle 파일에 stetho 라이브러리를 추가하고

 

implementation "com.facebook.stetho:stetho-okhttp3:1.5.1"

 

Application으로 선언된 클래스에 Stetho를 초기화해준다

 

public class DemoApp extends DaggerApplication {
@Override
    public void onCreate() {
        super.onCreate();
        Stetho.initializeWithDefaults(this);

 

3. 결과 

 

테스트 한 결과 영상 파일들을 쪼개서 보낸 요청들을 Stetho를 이용해서 확인 할 수 있었다

 

 

0. 소개

 

MotionLayout은 ConstraintLayout 안에 있는 객체들에 대해서 XML 파일들만 추가해서 간단하게 레이아웃 애니메이션 효과를 줄 수 있는 툴 정도로 이해하면 될 것 같다. 예제로 구글 소개글에 있는 예제들 봐도 되고 아래 gif 이미지를 참고해도 좋다. 2018년 말에 나온 기능인데 이제 와서 글을 쓰고 있으니 아주 뒷북인 감이 없지 않다.

 

스와이프로 빨간 사각형을 움직이는 애니메이션을 줄 수 있다.

1. 원리

 

MotionLayout 은 완전히 새로운 기능으로 도입 된 것은 아니고 원래 ConstraintLayout에서 애니메이션 효과를 주기 위해 사용한 ConstraintSet + TransitionManager를 좀 더 쉽게 사용할 수 있는 툴로 도입 됐다. ConstarintSet + TransitionManager에 대해 생소하신 분들은 이 유튜브 영상을 참고하자. 기존 효과보다 더 좋아진 점은 ConstraintSet + TransitionManager 조합에서는 간단한 효과의 경우에도 액티비티, 프래그먼트단에서 코드를 추가해야 했는데 MotionLayout을 사용하면 XML 코드단에서만 수정하면 돼서 변경의 범위를 최소화 할 수 있는 것 같다.

 

ConstarintLayout의 확장 기능으로 도입된 만큼 MotionLayout은 ConstraintLayout의 일부 요소들을 상속 받고 있다. 아래 그림에서 MotionLayout 의 기본 요소인 MotionScene이 ConstraintSet의 속성들을 포함하고 있는 것을 볼 수 있다. 실제 코드에서는 이 요소를 활용해 ConstraintLayout 내부의 객체들의 효과를 주는 일을 한다. 그 아래 Transition 속성을 보면 OnClick과 OnSwipe가 있는데 이는 클릭과 스와이프 인터랙션에 대해서 콜백을 줄 수 있는 것으로 이해하면 된다. 이 글에서는 MotionLayout은 ConstraintLayout 의 속성들을 이용해 애니메이션 할 수 있다는 점이란 것을 기억하고 자세한 내용은 개발 문서를 참고하도록 하자. 

 

2. 예제

 

MotionLayout을 사용하려면 우선 라이브러리를 추가해야한다. build.gradle에 아래의 코드를 추가해 최신 ConstraintLayout 소스를 불러오자. 번외로 2018년도 즈음에 MotionLayout이 ConstraintLayout 2.0.0 라이브러리에 포함되기 시작했는데 아직도 beta 버전에 머무르고 있는거 보니 다른 feature들과 같이 정식으로 릴리즈 되려는 모양인가보다.

 

implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'

 

정상적으로 라이브러리를 불러 왔다면 이제 레이아웃 파일을 수정할 때다. 아래 코드는 기존에 있던 xml 파일을 MotionLayout을 적용할 수 있도록 변경한 것이다.

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@anim/motion_scene"
    tools:context=".MainActivity">
    <View
        android:id="@+id/button"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:background="@color/colorAccent"
        android:text="Button"/>
</androidx.constraintlayout.motion.widget.MotionLayout>

 

원래는 ConstraintLayout을 사용했었는데 MotionLayout을 사용하고자 클래스 이름을 MotionLayout으로 변경했다. MotionLayout이 ConstraintLayout을 상속받은 클래스이기 때문에 자식 뷰에서 특별히 바꿔야할 것은 없다. 새롭게 추가한 코드는 app:layoutDescription인데 여기에 Motion 효과를 명시한 xml 파일을 넣었다. 

 

motion_scene 파일은 다음과 같다.

 

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">
    <Transition
        motion:constraintSetStart="@+id/start"
        motion:constraintSetEnd="@+id/end"
        motion:duration="1000">
        <OnSwipe
            motion:touchAnchorId="@+id/button"
            motion:touchAnchorSide="right"
            motion:dragDirection="dragRight" />
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/button"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="8dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/button"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginEnd="8dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>
</MotionScene>

Transition 속성을 보면 motion:constraintSetStart와 motion:constraintSetEnd 속성이 있는데 이는 애니메이션 효과 동안 constraint의 시작과 끝에 대한 정보를 나타낸다. 아래 코드 보면 두개의 ConstraintSet이 있는 것을 볼 수 있는데 시작할 때의 값과 끝의 값이 다른 것을 알 수 있다. 이 포스트 상단에 있는 gif 파일과 일치하는 것을 볼 수 있다.

 

그 아래 OnSwipe는 특정 뷰를 스와이프 할 때 줄 수 있는 효과를 명시했다. touchAnchorId의 값이 +@id/button으로 설정돼있는데 이는 MotionLayout에서 button 이란 id를 가진 뷰에게는 swipe 효과를 줄 것이라는 뜻이다. motion:touchAnchorSide와 motion:dragDirection이 있는데 이 값들을 이용해서 스와이프에 추가로 효과를 줄 수 있다.

 

3. 짧은 평

 

Android Studio 4.0에서는 XML파일에 MotionLayout 미리보기 화면에서도 애니메이션 효과를 보여줄 예정이라고 하니(4.0-beta 버전 참고) 구글에서도 MotionLayout을 사용하는 것을 적극 권장하는 것 같다. 앞으로 MotionLayout에서 추가된 기능이 나올 것 같으니 지금부터 프로젝트에 도입하는 것을 목표로 해야겠다.

0. Subject


RxJava에서 Subject 클래스는 구독하고 있는 관찰자(Observer)에게 새로운 값을 전달 할 때 사용하는 클래스다. 따로 Observable로 새로운 값을 만들 필요 없이 Subject 객체에 내장된 onNext 함수로 새로운 값을 옵저버에게 전달할 수 있기 때문에 짧은 코드로도 reactive하게 구현하는 것이 가능하다. 안드로이드에서 제공하는 LiveData와 유사한 역할을 한다.


아래 코드는 Subject 클래스중 하나인 PublishSubject를 이용해서 새로운 값을 갱신하는 예제다.

class Person {
    var publishName: PublishSubject<String>
            = PublishSubject.create()
}

val person = Person()
person.publishName.subscribe {
    Log.d(TAG, "publishName: " + it)
}
person.publishName.onNext("selfish")
person.publishName.onNext("developer")

실행결과 다음과 같이 수정된 값이 출력되는 것을 확인 할 수 있다.



1. PublishSubject vs BehaviorSubject


RxJava에서 제공하는 Subject 함수로 AsyncSubject, PublishSubject, BehaviorSubject, RelaySubject가 있는데 이번 포스트에서는 가장 많이 사용되는 PublishSubject와 BehaviorSubject를 그리고 둘 간의 차이를 소개해보려고 한다. 그런데 바로 글로 쓰는 것 보다는 코드와 출력되는 결과를 보면서 설명을 하는게 더 좋을 것 같다.

class Person {
    var behaviorName: BehaviorSubject<String>
            = BehaviorSubject.create()
    var publishName: PublishSubject<String>
            = PublishSubject.create()

    fun nextName(name: String) {
        behaviorName.onNext(name)
        publishName.onNext(name)
    }
}

person.nextName("selfish")
person.publishName.subscribe {
    Log.d(TAG, "publishName: " + it)
}
person.behaviorName.subscribe {
    Log.d(TAG, "behaviorName: " + it)
}
person.nextName("developer")

Person 클래스에는 BehaviorSubject 객체를 선언해뒀고 Subject 객체의 값을 한 번에 바꾸고자 nextName이라는 함수를 만들었다. 그리고 아래 코드에서는 publishName과 behaviorName을 구독하도록 했는데 기존 코드와 달리 구독하기 전에 이름을 "selfish"로 갱신을 미리 해뒀다. 


이 코드의 출력 결과는 다음과 같다.



BehaviorSubject로 선언 된 객체는 구독 전에 갱신한 "selfish" 문자열을 출력하는 반면 PublishSubject로 선언 된 객체는 구독 이후에 갱신한 "developer" 문자열만 출력하고 있다. 이는 두 객체의 동작 구조가 다르기 때문이다.


2. PublishSubject


PublishSubject 객체의 경우 구독 이후에 갱신된 값에 대해서만 값을 받는다. 아래 다이어그램의 세번째 줄에서 구독하기 이전에 갱신된 빨간공, 초록공은 무시하고 파란 공만 받고 있는 것을 볼 수 있다. 과거에 데이터를 무시하고 새로 갱신되는 값만 보고 싶은 경우 사용하기 유용하다. 대표적으로 버튼을 클릭하는 이벤트를 PublishSubject로 사용하기도 한다.



3. BehaviorSubject


BehaviorSubject 객체의 경우에는 구독하는 시점의 가장 최근에 갱신된 값을 받는다. 다이어그램 세번째 줄에서 구독하면서 가장 최근에 갱신된 초록색 공과 그 이후에 갱신된 파란색 공을 받는것을 볼 수 있다. 구독하는 시점에서 과거에 갱신된 데이터중 가장 최근의 값이 필요할 때 써먹으면 유용하다.



그림 출처


RxJava: defer, fromCallable

컴퓨터공부/안드로이드 2020. 2. 15. 16:16 Posted by 아는 개발자

1. defer


Observable 클래스내에 포함된 defer() 함수는 관찰하고 있는 대상의 값을 구독한 이후 시점부터 볼 때 사용한다. 즉 subscribe 함수가 불린 시점부터 대상의 값을 관찰한다. 좀 더 이해를 쉽게 하고자 Person이라는 클래스를 만들어봤다.

class Person {
    var name: String = "None"

    fun observableName(): Observable<String> 
            = Observable.just(name)

    fun observableDeferName(): Observable<String>
            = Observable.defer { Observable.just(name) }
}


Person 클래스에는 수정이 가능한 name 변수와 name을 Observable로 변환해주는 observableName() 함수, 그리고 코드는 거의 비슷한데 앞에 defer 함수가 붙어있는 observableDeferName() 함수가 있다. 이 두 함수의 차이를 알아보고자 onCreate 함수에서 Person 클래스에서 선언한 함수값을 이용해 다음과 같이 코드를 짜봤다. 두 함수를 통해 Observable 객체를 만든 후 person의 name 변수 값을 수정한 다음 Observable 객체에서 구독받은 값을 출력하는 간단한 예제다.

val person = Person()
val observableName = person.observableName()
val observableDeferName = person.observableDeferName()

person.name = "selfish developer"

observableName.subscribe {
    Log.d(TAG, "observableName: " + it)
}

observableDeferName.subscribe {
    Log.d(TAG, "observableDeferName: " + it)
}

실행 결과 출력되는 로그는 다음과 같다.



observableDeferName은 subscribe된 시점부터 값을 보기 때문에 수정한 person 객체의 name 값이 갱신되었고 observableName은 생성 시점의 값을 받아오기 때문에 초기 값으로 세팅된 값을 가져온다. 


그런데 아래 코드는 똑같은 값을 출력한다.

person.name = "selfish developer"
person.observableName().subscribe {
    Log.d(TAG, "person.observableName(): " + it)
}

person.observableDeferName().subscribe {
    Log.d(TAG, "person.observableDeferName(): " + it)
}


그 이유는 관찰하고 있는 두 객체의 생성 시점이 모두 name 변수값이 업데이트 된 이후기 때문이다. 관찰용 객체를 함수로 선언하지 않고 Person 클래스 내의 변수로 바꾸면 앞서 보인 예시와 동일하게 서로 다른 값을 출력하게 된다.

class Person {
    var name: String = "None"

    val observableName 
            = Observable.just(name)
    val observableDeferName 
            = Observable.defer { Observable.just(name) }
}

person.name = "selfish developer"

observableName.subscribe {
    Log.d(TAG, "observableName: " + it)
}

observableDeferName.subscribe {
    Log.d(TAG, "observableDeferName: " + it)
}


2. fromCallable


Observable 클래스의 형제격인 Maybe, Flowable, Single 클래스에서는 fromCallable 함수가 defer와 같은 역할을 한다. 테스트를 해보고자 Observable과 동일하게 코드를 짜봤다.

class Person {
var name: String = "None"

fun singleName(): Single<String>
        = Single.just(name)

fun singleCallableName(): Single<String>
        = Single.fromCallable { name }
}

val singleName = person.singleName()
val singleCallableName = person.singleCallableName()

person.name = "selfish developer"

singleName.subscribe { it ->
    Log.d(TAG, "singleName: " + it)
}

singleCallableName.subscribe { it ->
    Log.d(TAG, "singleCallableName: " + it)
}

실행 결과 defer와 동일하게 fromCallable이 붙은 함수는 구독한 시점 이후에 갱신된 값을 읽어온다.




3. 총평


실행 결과는 신기하기도 하지만 실제로 사용할때는 꽤 실수가 잦을 것 같은 기능인 것 같다. 가능하면 매번 새로운 Observable 객체를 생성하는 함수를 따로 변수로 만들어두지 않고 바로 구독하게 해 만들어서 갱신 타이밍 이슈를 피하는게 좋지 않을까 싶다.

RxJava: mapper function returned null 에러

삽질 기록 2020. 2. 14. 17:10 Posted by 아는 개발자

RxJava로 여러 객체의 변화를 보고 있다 보면 아래 파란 버그 처럼 The mapper function returned a null value 에러를 보게되는 경우가 종종 있다.



이 경우는 Observable 객체 내부의 map 함수에서 null을 리턴해주고 있기 때문에 발생한다. 앱이 죽는 크래쉬 에러까지는 아니지만 RxJava에서 null이 되는 경우에 대해 에러 로그를 출력한 만큼 map 함수에서 null이 발생할만한 경우를 사전에 막는 것이 좋다

TAG RxJava

코틀린 apply, also, let, run, with

컴퓨터공부/안드로이드 2020. 2. 9. 14:21 Posted by 아는 개발자

자바에 비해 코틀린이 가지는 가장 큰 장점은 코드를 간결하게 작성 할 수 있는 것이라고 생각하는데 모든 객체에 기본적으로 제공하는 범위함수인 apply, also, let, run, with 들이 이 이점을 살리는데 큰 도움이 된다.


이 함수의 차이점에 대해서 설명한 글은 코틀린 공식 문서도 있고 다른 개발 블로그에도 무수히 많지만, 범위 함수에서 강조하는 수신객체와 람다식과 관련된 내용은 문서를 읽는 것 보다는 직접 코드를 짜면서 체험해 볼 때 이해하기가 쉽다. 이번 포스트에서는 apply, also, let, run, with를 언제 사용해야하는지에 대해서 수신객체에 관련된 내용을 제외하고 사용이 필요한 경우만 간략하게 소개해보려고 한다.


1. apply 


apply는 객체의 property 값을 적용할 때 사용한다. 어떤 객체를 선언할 때 생성자만으로 값을 세팅할 수 없다면 apply를 통해서 값을 따로 붙여서 연속적으로 값을 세팅할 수 있다. 아래의 두 코드는 모두 동일한 결과를 가지는데 apply 함수를 사용한 경우가 더 명시적이다.


val adam = Person("Adam").apply { 
    age = 20     
    city = "London"
}

val adam = Person("Adam")
adam.age = 20
adam.city = "London"


2. also 


also는 속성 변경을 허용하지 않고 로그를 출력하거나 값의 유효성을 검증하고 싶을 때 사용한다. 아래 코드처럼 also 문 앞에 있는 코드는 property를 바꿀 수 없다. 나도 모르게 저지르는 실수를 사전에 차단하고 싶을 때 사용하면 유용하다.


val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")


3. let 


객체가 null이 아닌 코드를 실행하는 경우 사용한다. Null pointer 에러로 크래쉬가 나는걸 막을 때 꽤나 유용한 범위함수다. 주로 많이 사용하는 코드다. Java의 Optional.ofNullable 이렇게 길게 쓸 필요가 없어 간결해서 좋다.


val str: String? = "Hello"   
//processNonNullString(str)       // compilation error: str can be null
val length = str?.let { 
    println("let() called on $it")        
    processNonNullString(it)      // OK: 'it' is not null inside '?.let { }'
    it.length
}


4. run 


객체에 포함된 함수를 실행하고 그 결과를 반환할 때 사용한다. 아래 코드를 보면 리스트 타입인 numbers에 포함된 add 함수를 run 내부에서 실행하고 있고 마지막 구문에 e로 끝나는 문자열의 개수를 countEndsWithE 변수에 넣어주고 있다. 실행 결과는 주석으로 처리된 부분을 읽어보면 된다.


나는 이 함수는 자주 사용하지는 않는다. 굳이 함수를 먼저 실행한 다음에 리턴타입이 필요할 일도 없어서. apply랑 비슷한것 같기는 한데 그만한 유용성은 못찾겠다.


val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run { 
    add("four")
    add("five")
    count { it.endsWith("e") }
}
println("numbers: " + numbers)
println("There are $countEndsWithE elements that end with e.")
// numbers: [one, two, three, four, five]
// There are 3 elements that end with e.


5. with 


null이 될 수 없는 객체의 값을 출력할 때 사용한다. also랑 거의 차이가 없어서 나는 잘 사용하지 않는다. 


val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}


브런치나 틱톡처럼 수직으로 스와이프 해서 화면을 넘길 수 있는 ViewPager를 만들려고 인터넷에 검색해보면 ViewPager 클래스에서 상속받는 일부 함수들을 변경하는 답변이 많다. 그런데 대부분의 답변으로 시도해본 결과 아래 이미지처럼 스무스하게 스와이프가 되지 않는 문제가 있었다



0. 기존 ViewPager 클래스의 문제점


원인은 화면을 넘기는 부분을 담당하는 코드가 안드로이드 내부 라이브러리인 ViewPager 클래스 내에 있고 이쪽 코드는 수직으로 넘기는 걸 고려하지 않게 구현되어 있기 때문이다. ViewPager에서 일부 변수들을 오버라이드 할 수 있도록 하지 않았을 까 하는 일말의 기대가 있었지만 스와이프시 넘기는 부분의 threshold 값의 역할을 하는 변수는 오버라이드 할 수 없는 private 으로 구현이되어 있었다. 몇몇 답변에서는 super class의 변수를 바꾸는 방법을 제안했는데 이 방식은 구조적으로 좋은 방법도 아니거니와 최신 디바이스에선 통하지도 않는다.


1. ViewPager2 클래스


구글에서는 ViewPager 코드를 바꾸는게 귀찮았는지 ViewPager2라는 새로운 라이브러리를 내놓았다. ViewPager2는 기존 ViewPager 클래스가 가지고 있던 버그들을 잡고 신규 기능을 추가한 새로운 클래스인데 주요 기능으로 세로모드 지원이 있었다. 코드로 적용을 해보니 아래 그림처럼 간단한 스와이프로도 화면이 슉슉 넘어갈 수 있게 됐다. 



2. 구현방법


2.1 ViewPager2 라이브러리 다운 받기 

implementation 'androidx.viewpager2:viewpager2:1.0.0'

build.gradle에서 소스를 추가한다. 예전에는 alpha 태그를 붙였는데 지금은 정식 버전인 1.0.0이 나왔다.


2.2 ViewPager2용 adapter 구현 

public class ViewPager2Adapter extends FragmentStateAdapter {
    private List<string> stringList = new ArrayList<>();

    public ViewPager2Adapter(@NonNull FragmentActivity fragmentActivity, List<string> stringList) {
        super(fragmentActivity);
        this.stringList = stringList;
    }

    @NonNull
    @Override
    public Fragment createFragment(int position) {
        return MyFragment.newInstance(stringList.get(position));
    }

    @Override
    public int getItemCount() {
        return stringList.size();
    }
}

기존 ViewPager Adapter와 차이가 있는 부분은 상속 클래스인 FragmentStateAdapter 일 것이다. ViewPager2를 만들면서 구글에서 추가한 새로운 어댑터인데 정확히 어떤 차이점이 있는지는 아직 모르겠다.


2.3 XML에서 ViewPager2 소스 추가하기 

<androidx.viewpager2.widget.viewpager2 
android:id="@+id/viewPager" 
android:background="#000000" 
android:layout_width="match_parent" 
android:layout_height="match_parent"
</androidx.viewpager2.widget.viewpager2>

원래 ViewPager가 있던 자리에 ViewPager2로 바꿔준다.


2.4 Adapter 초기화 하고 Viewer에 붙이기

viewPager2 = findViewById(R.id.viewPager);
ViewPager2Adapter adapter = new ViewPager2Adapter(this, stringList);
viewPager2.setOrientation(ViewPager2.ORIENTATION_VERTICAL);
viewPager2.setAdapter(adapter);

만들어둔 어댑터를 ViewPager2 Viewer에 붙여준다. 여기선 수직으로 스와이프 하려고 setOrientation 함수에 수직 상수값을 넣어줬다. 수평으로 스와이프 하고 싶으면 값을 변경하면 된다.


3. 소스코드


https://github.com/kwony/ViewPager2-Vertically


'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

RxJava: defer, fromCallable  (0) 2020.02.15
코틀린 apply, also, let, run, with  (0) 2020.02.09
수직으로 스와이프 가능한 ViewPager 만들기  (0) 2019.12.07
RxJava - flatMap  (0) 2019.11.30
RxJava - Create 함수  (0) 2019.08.11
RxJava - Observable, Observer  (0) 2019.08.10

RxJava - flatMap

컴퓨터공부/안드로이드 2019. 11. 30. 15:23 Posted by 아는 개발자

RxJava에는 유용한 함수들이 많지만 그중에서 가장 많이쓰고 쓸모있는 것을 고르라면 나는 flatMap을 고르고 싶다. 시중의 RxJava/RxAndroid 강의에서도 다른 함수와 달리 flatMap을 소개 할 때는 분량을 길게해서 소개하는데 이해하기는 어렵지만 그만큼 쓰임새가 많아서 그런 것 같기도 하다.


1. flatMap




flatMap은 Observable 작업을 여러번 연계해서 사용할 때 사용하는 API다. 그러나 이렇게 추상적인 말로 설명하면 쉽게 와닿지가 않는다. 구체적인 예시를 통해서 살펴보도록 하자.


2. flatMap 예시


네이버 뉴스 페이지에서 특정 기사를 클릭하면 안드로이드 앱에선 기사의 텍스트, 사진 또는 동영상, 좋아요 개수, 댓글 정보 그리고 기사의 광고를 서버로부터 읽어올 것이다. 이때 이 정보들이 하나의 api에서 묶어져 있지 않고 텍스트 api 따로, 사진 동영상 api 따로, 좋아요와 댓글 정보 따로 있다면 각각 api로 호출해서 불러야 할 것이다.


각자 api를 따로 부른 다음 페이지에 표시한다면 굳이 flatMap을 쓸 필요는 없을 것이다. 그런데 기사를 불러오는데 실패한 경우에는 기사의 광고를 보여주지 않는다는 요구사항이 추가 됐거나 또는 클라이언트에서 서버에 한번에 여러 요청을 보내는 것이 부담을 주는 문제가 발생했다고 생각해보자. 이런 경우에는 서버로부터 기사 광고를 불러오는 작업이 기사를 불러온 작업 이후에 시행돼야 할 것이다. 


안드로이드 라이브러리를 이용해서 위 요구사항을 해결한다면 방법은 있긴 할텐데 코드양도 길어지고 신경써야 할 부분도 많아 귀찮을 것이다. 그런데 RxJava를 이용하면 한 줄만 추가하면 된다.


3. flatMap 코드 샘플

repository.getArticleContents("newsid")
    .doOnSuccess { it ->  }
    .flatMap { repository.getArticleAds("newsid", "ownerid")}
    .doOnSuccess { it -> }

위 코드는 getArticleContent 함수로 기사 내용을 읽은 다음에 다시 flatMap 함수에서 getArticleAds 함수에서 기사의 광고 내용을 호출한 코드다. 기사 광고를 flatMap을 통해 호출하게 되면 자동으로 기사 내용을 잘 읽어온 경우에만 호출하게 된다. 이처럼 RxJava를 적재적소에 이용하면 코드의 양이 줄어들고 직관적으로 코드를 짤 수 있게 된다

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

코틀린 apply, also, let, run, with  (0) 2020.02.09
수직으로 스와이프 가능한 ViewPager 만들기  (0) 2019.12.07
RxJava - flatMap  (0) 2019.11.30
RxJava - Create 함수  (0) 2019.08.11
RxJava - Observable, Observer  (0) 2019.08.10
안드로이드 Loader  (0) 2019.07.15

RxJava - Create 함수

컴퓨터공부/안드로이드 2019. 8. 11. 13:36 Posted by 아는 개발자

앞서 작성한 포스트에선 Observable의 역할이 어떤 데이터를 Observer가 처리할 수 있도록 포장해주는 역할 이라고 설명했다. Observable은 데이터를 관찰 할 수 있는 형태로 만들 기 위해 여러 가지 오퍼레이터 함수를 가지고 있다. 이번 포스트에선 이중에서 대표적으로 사용되는 것들만 소개해보려고 한다.


1. create


백그라운드 스레드에서 옵저버가 처리할 넘겨주는 방법. 아래 코드를 보면 create 함수의 인자로 익명 ObservableOnSubscribe 클래스를 선언하고 이 안의 오버라이드 함수 인자인 emitter 변수에 onNext로 0~9까지 값을 넣어 호출 하는 것을 볼 수 있다.


Observable<Integer> observable = Observable
        .create(new ObservableOnSubscribe<Integer>() {
            @Override
            public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
                Log.d(TAG, "subscribe: " + Thread.currentThread().getName());

                for (int i = 0; i < 10; i++) {
                    if (!emitter.isDisposed())
                        emitter.onNext(i);
                }

                if (!emitter.isDisposed())
                    emitter.onComplete();
            }
        })
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread());

observable.subscribe(new Observer<Integer>() {
    @Override
    public void onNext(Integer integer) {
        Log.d(TAG, "onNext: " + integer);
    }
});


실행결과 아래와 같은 로그가 출력된다. 0~9까지 값이 옵저버에게 전달 됐다. 추가적으로 subscribe에서 돌고 있는 쓰레드 이름을 출력해보니 RxCachedThreadScheduler-1이란 것이 출력됐다. 이는 subscribeOn 함수에 백그라운드 함수중 하나인 Schedulers.io()를 넣었기 때문이다. 이 함수를 사용하면 Observable한 데이터를 만드는 작업을 백그라운드에서 실행 할 수 있게 된다.



2. just 


최대 10개까지의 배열 데이터를 Observable하게 만들 수 있는 함수. 그런데 10개로 제한돼서 배열을 전달하는 경우는 없고 단일 데이터를 전달 할 때 주로 사용된다.


Observable<Integer> observable = Observable
        .just(1,2,3)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread());

observable.subscribe(new Observer<Integer>() {
    @Override
    public void onNext(Integer integer) {
        Log.d(TAG, "onNext: " + integer);
    }
});


이 함수를 실행해보면 다음과 같은 로그가 나온다.



3. range 


범위를 지정해주는 방법. for문을 생성 함수의 하나로 뒀다고 생각하면 쉽다. 단 range 함수를 사용하면 map같은 형변환 오퍼레이터를 사용하지 않으면 옵저버에선 Integer의 형태로 값을 받게 된다.


Observable<Integer> observable = Observable
        .range(0, 5)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread());

observable.subscribe(new Observer<Integer>() {
    @Override
    public void onNext(Integer integer) {
        Log.d(TAG, "onNext: " + integer);
    }
});


4. fromIterable 


이미 생성된 배열의 값을 Observable하게 바꿔주는 함수. 앞서 설명한 just는 최대 10개까지 밖에 담지 못한 반면 fromIterable은 개수에 상관 없이 모두 Observable하게 바꿔준다.


List<Integer> integers = new ArrayList<>();
integers.add(1);
integers.add(2);
integers.add(3);

Observable<Integer> observable = Observable
        .fromIterable(integers)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread());

observable.subscribe(new Observer<Integer>() {
    @Override
    public void onNext(Integer integer) {
        Log.d(TAG, "onNext: " + integer);
    }
});


'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

수직으로 스와이프 가능한 ViewPager 만들기  (0) 2019.12.07
RxJava - flatMap  (0) 2019.11.30
RxJava - Create 함수  (0) 2019.08.11
RxJava - Observable, Observer  (0) 2019.08.10
안드로이드 Loader  (0) 2019.07.15
onSaveInstanceState  (0) 2019.07.15

RxJava - Observable, Observer

컴퓨터공부/안드로이드 2019. 8. 10. 14:52 Posted by 아는 개발자

좀더 리액티브한(reactive) UI를 만들기 위해서 프로젝트에 RxJava, RxAndroid를 도입하면서 가장 어려웠던 점은 Observable과 Observer 클래스를 개념적으로 이해하는 것이었다. 옵저버 패턴을 응용한 라이브러리이기 때문에 익숙할 것이라고 여러 문서에서 설명하고 있는데 애초 옵저버 패턴을 많이 써보지 않았고 또 디자인 패턴 책을 여러 번 보고 난 뒤에 문서를 읽어봐도 쉽사리 와닿지 않았다.


여러 번의 블로그 방문과 삽질을 반복한 후 다행히 유튜브 강좌를 통해서 어느정도 개념을 잡을 수 있었다. 혹시나 나처럼 어려움을 겪고 계신 분은 Coding with Mitch 라는 유튜브 채널 강좌를 들어보시면 도움이 될 것 같다. 모두 듣고 소화하는데 하루 정도 소요되는데 개인적으로 충분히 들어볼만한 강좌였다.


서론이 너무 길었다. 아무튼 이번 포스트에선 앞서 말한 Observable과 Observer 클래스를 개념적으로 정리해보려고 한다. 개인 공부를 위해 정리한 글인 만큼 다른 블로그이나 문서에서 설명한 것과 차이가 있을 수 있을 것 같은데 혹시나 이 글을 읽으신 분들은 이점 참고하셨으면 좋겠다.


1. Observable


이 클래스에 포함된 함수는 매우 다양하나 어떤 데이터를 관찰 할 수 있는 형태로 바꾸는 것이 이 클래스의 기본적인 임무다. Observable의 의미인 '관찰 할 수 있는'을 생각해보면 이 클래스가 어떤 일을 해야하는지 짐작 할 수 있을 것이다. 이 클래스는 개념적으로는 어떤 데이터를 Observer가 처리할 수 있도록 포장하는 작업을 담당한다고 보면 이해하기엔 편하다. 추가적인 기능으로는 값을 변형시키고 다른 타입으로 바꾸기도 하고... 등등 많은데 일단 이 정도만 이해하도록 하자.


2. Observer


스타크래프트의 정찰용 옵저버 유닛을 생각하면 이해하기 어려워진다. 게임 유닛은 잊고 순수 의미인 '관찰자'의 의미에서 보면 Observer는 Observable에서 관찰 할 수 있는 형태로 전달한 데이터를 받고 이에 대한 행동을 취한다. 전달 받은 데이터를 가지고 화면 UI를 업데이트 하든지 아니면 어떤 인자를 서버에 요청해보는지 등등.. 최종적으로 처리할 작업은 이 클래스에서 담당한다


3. Observable & Observer Diagram


출처: 깃헙


위 그림을 보면 ObservableonNext, onComplete, onError 같은 함수를 통해 Observer에게 무언가를 전달하고 있는 것을 볼 수 있다. onNext는 데이터를 포장할 때마다  Observer에게 완료된 작업물을 전달하는 것이고 onComplete는 포장 작업이 끝날 때 호출하는 함수다. onError는 Observable 내에서 어떤 문제가 생겼을 때 호출되는 함수다.


4. 코드 


아래 코드를 보면 Observable 이라는 변수는 String형태의 Observable 클래스 타입이다. 앞서 설명한 내용 대로라면 Observable은 String 형태의 데이터 타입을 Observable 하게 만들어야 한다. 이를 위해 just라는 함수를 통해 관찰할 수 있는 형태로 포장할 값으로 "selfish"와 "developer"를 넣었다. subscribeOn과 observeOn 함수는 어떤 쓰레드에서 작업을 실행할 지 정하는 함수인데 이번 포스트와는 관련이 없으니 일단 무시하자.


선언 후 subscribe 함수 내에 익명의 Observer 객체를 선언했다. 이는 만들어둔 Observable과 Observer를 매핑하는 함수다. Observer 객체를 보면 onNext, onComplete 처럼 앞의 그림에서 설명한 함수들이 등장한다. Observable에서 전달 받은 작업을 처리하기 위한 함수들이다.


Observable<String> observable = Observable
        .just("selfish", "developer")
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread());

observable.subscribe(new Observer<String>() {
    @Override
    public void onSubscribe(Disposable d) {

    }

    @Override
    public void onNext(String s) {
        Log.d(TAG, "onNext: " + s);

    }

    @Override
    public void onError(Throwable e) {

    }

    @Override
    public void onComplete() {
        Log.d(TAG, "onComplete: ");
    }
});


위 코드를 안드로이드 에뮬레이터로 실행해보면 다음과 같은 로그가 나온다.




'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

RxJava - flatMap  (0) 2019.11.30
RxJava - Create 함수  (0) 2019.08.11
RxJava - Observable, Observer  (0) 2019.08.10
안드로이드 Loader  (0) 2019.07.15
onSaveInstanceState  (0) 2019.07.15
JAVA의 static  (0) 2019.04.03

안드로이드 Loader

컴퓨터공부/안드로이드 2019. 7. 15. 21:20 Posted by 아는 개발자

앞선 포스트에서 onSaveInstanceState 콜백을 통해 화면을 전환하는 경우에도 데이터를 저장할 수 있는 방법을 배웠다. 그런데 AsyncTask 처럼 진행중인 작업에 대해서는 데이터를 저장할 수 있는 기능이 무의미할 것이다. 어디까지 데이터 작업을 처리 했으니 이때부터 다시 시작하라고 세세하게 할 수도 없는 노릇이고.


그래서 안드로이드에서는 Loader라는 라이브러리를 뒀다. 공식 문서에서는 FragmentActivity에 넣을 디스플레이 소스를 로드할 수 있는 기능으로 소개되고 있는데 일단은 별도의 쓰레드에서 돌아 Activity 생성 주기에 영향을 받지 않는 컴포넌트 정도로 이해하면 될 것 같다. 사용법은 아래와 같다.


1. implements LoaderManager.LoaderCallbacks<String>


MainActivity는 Loader API를 사용하는 Activity임을 명시 해둬서 Loader 콜백 함수들을 호출하도록 만든다.

public class MainActivity extends AppCompatActivity implements
        LoaderManager.LoaderCallbacks<string> { {

2. public Loader<String> onCreateLoader 


Activity에서 사용할 Loader 객체를 생성하는 콜백 함수를 구현한다. Loader는 AsyncTaskLoader와 CursorLoader가 있는데 CursorLoader 의 경우에는 DB에서 값을 읽어올 때 사용하고 AsyncTaskLoader 는 좀더 범용적으로 사용된다. 


2.1 protected void onStartLoading()


AsyncTaskLoader 가 생성 되면서 가장 먼저 실행되는 함수다. AsyncTask의 onPreExecute() 의 역할을 하는 것과 비슷하다. 백그라운드 작업 실행 하기 전에 필요한 셋팅 작업을 여기에 넣는다.


2.2 public String loadInBackground()


백그라운드 작업을 실행하는 함수다. 이름을 보면 감이 오겠지만  AsyncTask의 doInBackground(Void... voids)  와 동일한 기능을 하는 함수다. 반환 타입으로 세팅된 String은 결과 값의 타입이며 앞서 콜백을 implements 할 때 어떤 타입을 넣느냐에 따라 바꿀 수 있다.


2.3 public deliverResult(String result)


결과 값을 전달하는 함수다. 이 함수내에는 반드시 super.deliverResult(result);  가 포함되어 있어야지 결과 값이 최종적으로 전달 된다. 인자인 result 는 loadInBackground()에서 반환한 값이다


3. public void onLoadFinished(Loader<String> loader, String data)


AsyncTaskLoader 작업이 끝난 후에 불리는 함수이며 함수의 인자로 결과 값을 전달 받는다.  결과값을 화면에 업데이트 할 때 이 콜백 함수 내에 작업을 넣는다.


4. initLoader,restartLoader


생성한 Loader가 실행 될 수 있도록 호출한다. 아래 코드는 initLoader,restartLoader 함수를 실행한 예제 코드다.


LoaderManager loaderManager = getSupportLoaderManager();
Loader<string> searchLoader = loaderManager.getLoader(SEARCH_LOADER);
if (searchLoader == null) {
    loaderManager.initLoader(SEARCH_LOADER, queryBundle, this);
} else {
    loaderManager.restartLoader(SEARCH_LOADER, queryBundle, this);
}

함수의 첫번째 인자 값은 ID다. Loader마다 가지고 있는 고유한 Key값에 해당한다. 두번째 인자 값은 Bundle형태의 데이터 값이다. AsyncTaskLoader에게 이 데이터 값을 통해 값을 전달 할 수 있다. 세번째는 콜백함수다. 현재는 Activity가 Loader 콜백 함수를 구현해뒀기 때문에 this로 입력했다.

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

RxJava - Create 함수  (0) 2019.08.11
RxJava - Observable, Observer  (0) 2019.08.10
안드로이드 Loader  (0) 2019.07.15
onSaveInstanceState  (0) 2019.07.15
JAVA의 static  (0) 2019.04.03
안드로이드 Service  (0) 2019.03.19

onSaveInstanceState

컴퓨터공부/안드로이드 2019. 7. 15. 20:39 Posted by 아는 개발자

onCreate() onDestroy() 는  애플리케이션이 생성될 때와 종료 될 때 한 번씩만 불리는 콜백 함수로 알려져 있지만 디바이스의 설정 값이 갑자기 바뀌어 화면을 처음부터 새로 그려줘야 할 때도 불린다. 대표적으로 스마트폰을 회전 시키는 경우(rotate)가 이에 해당한다. 



확인해보기 위해 테스트 애플리케이션의 콜백 함수들에 로그를 넣고 에뮬레이터(오레오 8.1) 에 설치한 후 오른쪽으로 회전 해봤다. onPause() 함수부터 불리는 부분이 회전 후에 나온 로그며 이중에는 onCreate() onDestroy()도 포함되어 있다.


onCreate() onDestroy() 함수가 다시 불린다는 뜻은 회전하기 전까지 설정해둔 변수 값들이 모두 초기화 된다는 것을 의미하기도 한다. 만약 애플리케이션 화면에 특정 값을 바꾼 상태로 회전을 했다면 처음 애플리케이션을 실행한 상태로 화면이 바뀌기 때문에 지금까지 작업한 것들이 모두 날라가게 된다.


안드로이드에선 이런 상황을 대처하기 위해 onSaveInstanceState(Bundle outState) 라는 콜백 함수를 뒀다. 이 함수는 모든 인자들이 초기화 되는 onDestroy() 함수 호출 전에 실행되며 함수의 인자에 key-value로 여러가지 데이터를 넣을 수 있다. 변경된 인자는 최종적으로 onCreate(Bundle savedInstanceState) 함수의 인자 값으로 전달 된다.


구구절절히 코드로 보는 것이 더 이해하기 쉬울 것 같다. 먼저 onSaveInstanceState 함수내에 아래와 같이 임의의 문자열 데이터 값을 입력했다.

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    logAndAppend("onSaveInstanceState");

    outState.putString(SAVE_INSTANCE_KEY,
            "onSaveInstanceState is called!\n");
}

그리고  onCreate 함수에선 인자에 key 값이 저장되어 있는지 확인 한 후 있으면 그 값을 TextView에 표시하도록 했다. 

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

    mLifecycleDisplay = (TextView) findViewById(R.id.tv_lifecycle_events_display);

    if (savedInstanceState != null
            && savedInstanceState.containsKey(SAVE_INSTANCE_KEY)) {
        String savedString = savedInstanceState.getString(SAVE_INSTANCE_KEY);
        mLifecycleDisplay.setText(savedString);
    }
    logAndAppend(ON_CREATE);
}

그 결과 아래와 같이 회전 후 TextView에 로그 메시지가 추가되는 것을 확인 할 수 있었다



'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

RxJava - Observable, Observer  (0) 2019.08.10
안드로이드 Loader  (0) 2019.07.15
onSaveInstanceState  (0) 2019.07.15
JAVA의 static  (0) 2019.04.03
안드로이드 Service  (0) 2019.03.19
AsyncTask  (0) 2019.03.13

오픈소스 라이센스 정리

컴퓨터공부/오픈소스 2019. 6. 9. 11:04 Posted by 아는 개발자

오픈소스를 이용해서 개발하는 경우 코드를 무료로 보고 사용할 수 있어 개발하기 편리하다는 장점이 있으나 사용하고 있는 오픈소스가 어떤 라이센스를 가지느냐에 따라서 상업적인 이용이 제한될 수도 있고 내가 만든 코드를 공개해야 할 의무까지 생길 수 있다. 이런 경우를 예방하려면 개발하기 전부터 사용할 오픈소스 라이센스에 대해 검토해둘 필요가 있다. 이번 포스트에서는 유명한 오픈소스 라이센스들에 대해서 간단히 정리를 해보려고 한다.


0. 라이센스란? 


소프트웨어의 지적 재산권을 일컫는 말이다. 음악의 저작권과 비슷한 개념 정도로 생각하면 될 것 같다. 처음 코딩에 입문하는 사람들은 남들이 짠 코드랑 본인이 짠 것과 함수와 변수 명만 제외하면 거의 차이가 없어(헬로 월드 수준이니까) 딱히 저작권이라고 할만한 것이 있는지도 모르겠고 또 수만줄이 넘는 코드에서 카피 유무의 확인조차 불가능 할 것 같은 소프트웨어에 지적 재산권이 통용되고 있다는 것에 의문을 품기도 하지만 실제 소프트웨어 업계에선 엄연히 라이센스 규정에 따라서 운영되고 있으며 이미 몇가지 판례를 통해 법적인 효력까지 갖추고 있기 때문에 쉽게 무시해선 안된다



사용하고 있는 오픈소스가 어떤 라이센스를 쓰고 있는지 확인하려면 소스코드 파일의 맨위 주석을 보면 된다. 어떤 이유인지는 모르겠으나 거의 대부분의 라이센스가 파일 최상단에 명시돼 있다. 위 그림은 리눅스 커널 파일중 하나를 gedit으로 열어본 사진이다. 이 파일은 GNU 라이센스를 사용하고 있다.


1. GPL 라이센스


소스코드 공개 필요 O, 동일한 라이센스 적용 O, 상업적이용 O


GNU라고도 불리는 이 라이센스는 리눅스 커널에 기본 라이센스로 채택되면서 개발자 사이에선 유명한(악명높은) 라이센스가 됐다.. 이 라이센스는 오픈소스의 철학에 기초해서 만들어진 것이기 때문에 '자유를 누린 만큼 너의 코드도 공개해!' 원칙을 가지고 있다. 이 라이센스를 이용해서 만든 소프트웨어는 동일한 GPL 라이센스를 사용해야하고 모두 코드를 공개해야 한다. '내가 얘네 썼는지 어떻게 알겠어' 하면서 무시할 수도 있지만 이미 판례를 통해 법적인 효력까지 갖춘 라이센스이기 때문에 사용할 때는 신중해야 한다. 자세한 내용은 나무위키 전염성 조항 참고하면 좋다. 리눅스 커널 기반 안드로이드 폰 제조사들이 다른 코드는 몰라도 리눅스 커널 소스코드는 공개할 수 밖에 없는 것은 GPL 라이센스 덕분이다.


2. BSD 라이센스


소스코드 공개 필요 X, 동일한 라이센스 적용 X, 상업적 이용 O 


BSD는 자유 소프트웨어 저작권의 한가지로 BSD계열(미국 캘리포니아 대학 버클리에서 개발한 운영체제인 유닉스)에서 주로 채택하고 있는 라이센스다. 규정은 앞서 언급한 GPL과 확연하게 다른데 이 오픈소스 라이센스를 가진 코드는 자유롭게 사용할 수 있고 소스코드를 공개 하지도 않아도 되며 돈받고 팔아도 된다. BSD에서 눈여겨 볼 만한 것은 다루는 것은 누구나 자신의 용도로 사용할 수는 있지만 이 소프트웨어를 사용해서 발생 가능한 모든 리스크와 손해는 본인이 책임지도록 하고 있다. 책임 도피형 라이센스다.


3. MIT 라이센스


소스코드 공개 필요 X, 동일한 라이센스 적용 X, 상업적 이용 O


라이센스 이름에서 추측할 수 있듯이 매사추세스 공과대학에서 만든 소프트웨어 라이센스다. BSD를 기초해서 만든 라이센스라 거의 규정은 똑같다. 소스코드를 공개할 필요도 없고 동일한 라이센스를 적용할 필요도 없으며 이 소프트웨어를 사용해서 발생할 수 있는 손해에 대해서도 책임을 지지 않는다는 것 까지 동일하다. BSD 계열의 소프트웨어에서 일반 소프트웨어로 옮겨오기 위해 만든 라이센스 정도로 생각하면 될 것 같다.


4. Apache 라이센스 


소스코드 공개 필요 X, 동일한 라이센스 적용 X, 상업적 이용 O 


아파치(Apache) 소프트웨어 재단에서 자체적으로 만든 라이센스다. 안드로이드 프레임워크쪽의 대부분의 라이브러리가 이 라이센스 규정을 따르고 있다. BSD와 거의 규정이 동일해 소스코드를 공개할 필요가 없다. 차이점이 있다면 Apache 라이센스는 특허권 측면에서 좀더 완성도를 높여서 Apache 라이센스로 출원한 특허에 대해서는 소스코드 사용자에게 특허의 무제한적 사용을 허가한다는 규정을 담고 있다. 즉 어떤 소프트웨어가 아파치 라이센스를 채택해서 배포했다면 그 소프트웨어가 특허출원이 되어 있어도 사용자에게 특허 사용료를 요구할 수 없다는 뜻이다. 나무위키에 따르면 소스코드를 무료로 공개해놓고 그걸 빌미 삼아 특허권 소송을 제기하는 더티한 플레이를 막기 위한 규정이라 한다. 이런 법적인 안전장치까지 있어서 개발자들이 가져다가 쓸 때 가장 선호하는 라이센스 중에 하나인 것 같다.


5. 추가로 


상세한 규정을 알고 싶으신 분들은 블로그 사이트를 찾는 것보다 정보통신산업 진흥원에서 만든 책자를 보는 것이 도움이 될 것 같다. https://www.oss.kr/oss_license 사이트에 가면 라이센스 가이드를 다운로드 받을 수 있다. 법적인 문제와 연관 될 수 있으니 가능하면 전문가와 상담하는 것이 좋을 것 같다.

'컴퓨터공부 > 오픈소스' 카테고리의 다른 글

오픈소스 라이센스 정리  (0) 2019.06.09
FFmpeg  (0) 2018.10.31
자동차시장 오픈소스 - 2  (0) 2017.01.31
자동차 시장 오픈소스 - 1  (0) 2017.01.15
이런 오픈 소스도 있다!  (0) 2017.01.07
오픈소스 시작하기  (0) 2017.01.01

Virtio Block 성능 세부 분석

컴퓨터공부/가상화기술 2019. 5. 20. 20:28 Posted by 아는 개발자

예전 포스트에서는 iozone을 이용해 Virtio block 드라이버의 성능을 간단하게 측정해봤다면  이번 글에는 범용적으로 사용되는 스토리지 벤치마크인 fio를 이용해 Virtio Block의 성능을 좀더 디테일하게 분석해보려고 한다.


실험의 큰 단위를 Sequential, Random으로 나누고 각각의 I/O size를 바꿔봤을 때 Host와 VM의 성능 차이가 어느 정도 나오는지를 분석 해봤다.


1. Seq 512K, Rand 4K


 

 Host

VM 

Ratio 

Random Read 

7705MB/s 

5184MB/s 

67% 

Random Write 

120MB/s 

69.9MB/s 

58% 

Sequential Read 

13.5GB/s 

12.4GB/s 

91% 

Sequential Write 

501MB/s 

346MB/s 

69% 


Sequential에는 512K의 io size를, Random인 경우에는 4K 단위로 io size를 줬다. 이 값들은 실제 스토리지 벤치마크에서 자주 사용되는 단위다. 


결과 값의 편차가 크기는 하지만 Sequential의 경우가 Random의 경우보다 성능이 더 잘 나오는 것으로 보인다. 그런데 random이 sequential 대비 성능이 이렇게 떨어지는 것은 이상해 보인다. 읽는 과정은 크게 차이가 없어 보이는데 말이다.


2. Random 512K 


 

 Host

VM 

Ratio 

 Random Read

13.3GB/s 

12.5GB/s 

93% 

 Random Write

524MB/s 

366MB/s 

70% 


Random Access가 원인이기 보다는 I/O size의 크기에 따라 성능이 달라지는 것 같아서 Random의 경우에도 Sequential과 동일하게 I/O 사이즈를 512K로 줬다. 그러자 Sequential 때와 거의 비슷한 수준으로 성능이 나타났다. I/O 사이즈에 따라서 성능이 들쭉날쭉한다. 


3. Virtio Block Process Sequence




간략하게 표현하면 Virtio Block 드라이버의 처리 루틴은 위와 같은 방식으로 이뤄진다. 실제 스토리지 장치에 접근하는 Block Driver는 Native와 똑같은 장치를 사용하고 있으니 이 지점에서는 Native와 Virtual Machine 모두 똑같은 원리로 동작해 성능 차이가 나지 않는다. 오버헤드가 발생할 수 있는 부분은 이 지점 이외의 부분, VM에서 Block 드라이버까지 요청이 전달 되는 화살표로 표현한 과정에서 발생한다. 화살표 요청의 횟수가 적을수록 Native에 비슷한 성능을 내고 많을수록 저조한 성능을 보인다.


4. I/O operations between 4K random and 512K random 


QEMU의 trace 기능을 이용해 4K일 때와 512K일 때 Virtio Block 함수를 호출하는 횟수를 측정 해봤다.


 

 4K

512K 

 4K / 512K

 Random Read

 279727

25733 

 10.87 : 1

 Random Write 

 3920485

141694 

 27.6687


Read의 경우에는 4K의 경우가 512K보다 10배 정도 더 Virtio Block 관련 함수를 호출하고 Write의 경우에는 27배 더 호출하고 있다. 호출 횟수와 실제 성능과 비교해보면 Random Read의 Host 대비 성능(67%)이 Random Write의 결과값(58%)보다 높게 나온 것으로 보아 Virtio Block 함수 호출 횟수와 어느정도 비례하고 있는 것을 알 수 있다. 


'컴퓨터공부 > 가상화기술' 카테고리의 다른 글

Virtio Block 성능 세부 분석  (0) 2019.05.20
Virtio Block 성능 벤치마크  (0) 2018.12.23
QEMU를 이용해 커널 이미지 바꿔서 부팅해보기  (0) 2018.12.20
kvm ioeventfd  (0) 2018.08.11
kvm irqfd  (0) 2018.08.11
vhost  (0) 2018.07.08
TAG Block, FIO, virtio

SDN과 NFV

컴퓨터공부/클라우드컴퓨팅 2019. 4. 21. 13:28 Posted by 아는 개발자

SDN (Software Defined Network)

SDN은 "네트워크 자원을 소프트웨어적으로 가상화해서 운영하는 기술이다" 라고 한줄만 가지고 이해하기 모호하니 딱 정의하기에 앞서 이 기술이 나오게 된 배경부터 주목해보자. 지금과 같이 인터넷이 크게 발달하기 전에는 트래픽의 양이 많지 않았고 트래픽의 패턴도 서버와 클라이언트에게 주고받는 데이터 정도로 단순했다. 그런데 스마트폰의 시대가 시작되면서 트래픽의 양이 급격히 증가하기 시작했고 패턴도 단순히 클라이언트와 주고 받는 것이 아니라 다른 서버의 데이터베이스에 접근하는 경우가 생기기 시작했다. 이전보다 네트워크 관리 하는 것이 훨씬 복잡해졌다.

과거에는 네트워크 장비들이 모두 하드웨어로만 조작 할 수 있었다. 트래픽이 감당 할 수 없을 정도로 많아진 경우에는 서버를 더 사야했고 다른 서버로 연결 할 때는 연결시 필요한 방화벽, 스위치 등등도 추가로 구매해야 했다. 이런 경우들은 비용문제도 있을 뿐만 아니라 기존 서버에 새로운 하드웨어를 증설하는 일이 꽤나 시간도 오래 걸리고 번거롭다는 문제가 있었다. 가상화 기술이 나오면서 네트워크 장치들도 소프트웨어적으로 해결해 관리를 편하게 하려는 움직임이 있었는데 이런 요구사항으로 나오게 된 것이 SDN이다. 


출처: 주니퍼

SDN을 사용하면 서버를 여러대를 살 필요 없이 한번에 성능 좋은 서버를 산후 여러개의 VM을 돌려서 복수의 서비스를 돌릴 수 있다. 특정 서비스에 트래픽이 몰리면 이전처럼 서버를 추가로 증설할 필요 없이 트래픽이 몰리는 VM에 하드웨어 자원을 더 많이 할당해서 해결 할 수 있다. 예전처럼 하드웨어적으로 처리할 필요가 없어 관리하기가 쉬우며 놀고 있는 다른 VM의 자원들을 끌어다가 사용할 수 있기 때문에 비용적으로도 효율적이다.


NFV (Network Function Virtualization)

NFV는 앞서 설명한 SDN을 소프트웨어적으로 구현할 수 있는 기술이다. 구체적으로 설명하면 switch, firewall 같은 표준화된 네트워크 장비들을 소프트웨어적으로 만들수 있으며 서버에 설치된 네트워크, 스토리지, CPU 등 컴퓨팅 리소스의 orchestration을 담당하는 컴포넌트다. SDN이 NFV를 포함하는 기술이라고 보면 될 것 같다. 사실은 거의 전부라고 생각되긴 하지만.

출처: 주니퍼

세세하게 들어가면 끝도 없으니 위 그림의 컴포넌트들에 대해서 짤막하게만 알아두고 가자.

VNF (Virtualized Network Functions)

가상화 해서 만든 네트워크 장비들이다. 가상의 방화벽, 스위치, 로드밸런서 등등이 이에 해당한다. 필요에 따라서 여러 개를 만들 수 있다. 클라우드 환경에서 돌아가고 있는 서비스들이 바라보는 네트워크 장비이기도 하다.

NFVI (NFV Infrastructure)

앞서 설명한 VNF와 실제 하드웨어 장비간의 인터페이스를 담당한다. Virtualization Layer를 통해서 물리 장비에 매핑 할 수 있는 컴포넌트다. API라고 생각하면 좋을 것 같다.

NFV MANO(Management and Orchestration)

가상의 네트워크 장비(VNF)를 NFV 인프라스트럭처 (NFVI)를 이용해 실제 하드웨어에 매핑하고 NFV에서 자원 할당 문제로 번번히 언급되는 VNF 스케줄링, VNF 연결문제(Chain Composition) 등등을 다루는 핵심 컴포넌트다. 

'컴퓨터공부 > 클라우드컴퓨팅' 카테고리의 다른 글

SDN과 NFV  (0) 2019.04.21
Kubernetes 소개  (0) 2018.06.23
오픈스택 구조 분석  (0) 2017.03.07
오픈스택이란?  (0) 2017.03.07
클라우드 가상화와 Docker  (2) 2017.03.05
클라우드 컴퓨팅(Cloud Computing)  (0) 2017.02.25
TAG NFV, SDN

JAVA의 static

컴퓨터공부/안드로이드 2019. 4. 3. 21:50 Posted by 아는 개발자

0. JAVA Static

절차 지향인 C언어에서 static 변수는 함수 또는 변수를 작성 중인 파일 내에서만 사용하고 싶을 때만 사용하기 때문에 사용법이 비교적 간단했는데 JAVA에서는 객체지향 개념과 연관되어 있어 사용 시 다소 주의가 필요하다. 이번 포스트에서는 JAVA 언어에서 static의 쓰임새에 대해서 정리를 해본다.


1. Static/Non-static 멤버

자바에서는 클래스 내의 함수 또는 변수를 멤버(Member)라고 통칭해서 부르며 앞에 static 변수를 붙이면 static 멤버, 붙이지 않으면 Non-static 멤버로 분류한다.

public class MainActivity extends AppCompatActivity {
  static int staticVar = 0;       // static Member
  static void staticMethod() {}   // static Member

  int nonStaticVar = 0;           // Non-static Member
  void nonStaticMethod() {}       // Non-static Member
}

Static 멤버는 별도의 객체 생성과정 없이 바로 사용할 수 있고 클래스당 하나씩만 존재해 동일한 클래스로 생성된 복수의 객체가 동일한 변수의 값을 공유하게 된다. C/C++의 전역 변수와 함수와 원리가 비슷한데 이와 같은 특징은 Static 멤버들이 객체를 생성하기 전에 이미 메모리에 로딩되며 객체가 제거와 상관없이 프로그램이 종료될 때까지 메모리에 남아있는 구조적인 특징 때문에 발생한다.

Non-Static 멤버는 Static 멤버와 원리가 정반대다. 반드시 객체를 생성한 후에 사용할 수 있으며 생성된 객체별로 멤버의 값이 다를 수 있다. 이는 멤버 인스턴스들이 객체의 생명주기에 따라서 메모리에 생성되고 제거되는 원리 때문이다. Non-Static 멤버들은 객체지향적인 관점에 근거해서 동작하는 인스턴스다.


2. 사용 예시

2.1 Singleton Pattern

프로그램에서 클래스당 하나의 객체만 사용하도록 유도하는 Singleton Pattern은 static을 적절히 사용한 대표적인 예다. getInstance 함수를 별도의 객체 생서 없이 호출할 수 있도록 static 멤버로 선언했으며 클래스 변수인 instance도 static으로 선언해 여러개의 객체가 생성되더라도 동일한 객체만 리턴할 수 있도록 했다.

public class Singleton {
  private static Singleton instance;
  private LazySingleton() {}
  public static synchronized Singleton getInstance() {
      if(instance == null) {
          instance = new Singleton();
      }
      return instance;
  }
}

2.2 Math

private int getAbs(int a) {
	return Math.abs(a);
}

별다른 객체 생성 없이 바로 실행하는 Math 클래스의 함수들은 모두 static 멤버들로 선언된 것들이다. JAVA는 객체지향 관점에 따라서 모든 함수들은 반드시 객체를 통해서 호출되기 때문에 위와 같이 static 함수를 적극적으로 활용해 라이브러리를 호출할 수 있도록 했다.


3. 주의사항

3.1 Out of Memory

객체가 제거되도 static 멤버는 메모리에 남아있기 때문에 코드 상에서 별도로 처리해두지 않으면 모든 메모리 공간을 static 인스턴스가 차지하는 메모리 Leak이 발생할 수 있다.

public class OomClass {
  static Vector oomVector;
  static void addOomVector(int a) {
      oomVector.add(a);
  }
  public OomClass() {
      for (int i = 0; i < 100000; i++)
          addOomVector(i);
  }
}

위의 OomClass는 생성 할 때마다 static 변수는 oomVector 벡터 클래스에 0~99999까지 엔트리를 추가하는데 OomClass 한 두 개 정도 생성할 때는 별 문제가 되지 않지만 그 수가 많아지면 메모리에서 차지하는 비율이 높아져 OOM이 발생할 수 있다.

3.2 Static 함수의 제약사항

Static 함수 내에서는 Static 멤버만 사용할 수 있고 this 호출이 불가능하다. 이런 제약사항은 역으로 Non-Static 멤버도 접근 할 수 있고 this를 호출 할 경우 어떤 문제점이 발생할지 예측해보면 이해하기 쉽다. Non-Static 멤버들은 객체가 생성되어야 메모리에 한자리 차지하게 되는데 객체 생성 없이 바로 호출되는 static 함수에서 생성 됐는지 확인되지 않는 멤버 변수들에게 접근한다면 이는 invalid access가 된다. 구조적인 특징에 대해서 다시 한번 생각해보게 된다.



'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

안드로이드 Loader  (0) 2019.07.15
onSaveInstanceState  (0) 2019.07.15
JAVA의 static  (0) 2019.04.03
안드로이드 Service  (0) 2019.03.19
AsyncTask  (0) 2019.03.13
ViewPager와 PageAdapter  (0) 2019.03.05

안드로이드 Service

컴퓨터공부/안드로이드 2019. 3. 19. 21:42 Posted by 아는 개발자

0. 소개


Service 클래스는 안드로이드 앱에서 백그라운드 작업을 수행할 때 대표적으로 사용하는 라이브러리다. 앞서 설명한 AsyncTask와는 사용법이 완전히 다른데 2~3초 이내로 짧게 끝내야하는 AsyncTask와는 달리 Service는 오래도록 실행 할 수 있으며 AsyncTask처럼 UI 쓰레드와 Interaction 할 수 있는 기능은 없다. 그래서 화면과 무관한 작업인 네트워크 트랜잭션 처리나 음악을 재생할 때 사용된다.


1. 생성주기


액티비티와 독립된 생성주기를 가지고 있으며 별도의 컴포넌트로 동작하기 때문에 애플리케이션을 사용하지 않은 상태(UI가 화면에 있지 않은 상태) 여도 백그라운드에서 실행 될 수 있다. 폰 게임을 하는 중에도 카카오톡 메시지가 오고 쿠팡에서 쇼핑을 하는 도중에도 멜롬의 음악을 들을 수 있는 이유가 바로 이런 서비스 클래스의 특징 덕분이다. 경우에 따라선 애플리케이션 내에서 별도의 프로세스로 분리할 수도 있다.


2. 콜백함수 


Service 클래스의 콜백 함수를 통해 간단히 사용 방법을 익혀보자.


public void onCreate()


서비스가 생성 될 때 가장 먼저 실행되는 함수. 생성시 딱 한번만 호출되며 서비스 동작에 필요한 인스턴스를 선언하는 작업으로 주로 사용 한다.


public void onDestroy()


서비스가 종료될 때 호출되는 함수. onCreate에서 수행한 작업과 반대로 서비스가 종료하면서 인스턴스에 정리할 작업을 처리하는 일을 한다.


public int onStartCommand(Intent intent, int flags, int startId)


사용자의 요청을 처리하는 함수. 서비스를 처음 생성할 경우에는 onCreate 함수 다음에 불리나 생성 이후에는 이 함수로 바로 불린다. 전달 인자인 Intent에 필요한 작업을 전달할 수 있다.


3. 주의사항


단 주의할 것이 무한루프처럼 오래 걸리는 작업을 onStartCommand에 넣으면 ANR(Application Not Responding)에러가 발생하게 된다. 오래 실행하는 작업을 수행할 때 사용되는 라이브러린데 정작 사용하려니 에러가 발생하는게 생뚱맞게 들릴 수 있는데 이는 서비스 클래스 컴포넌트의 구조적인 이유 때문이다. onStartCommand함수는 서비스 작업 뿐만 아니라 사용자 interaction도 처리하는 메인쓰레드에서 담당하는데 이 함수가 빨리 종료되지 않는다면 사용자는 UI 작업을 처리하지 못하게 된다.


이러한 이유 때문에 오래 걸리는 작업은 onStartCommand에서 처리하되, 아래의 안드로이드 소스처럼 별도의 쓰레드를 생성해서 작업을 처리하도록 한다.


 
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    new Thread() {
        @Override
        public void run() {
            while (true) {
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
    }.start();

    return START_STICKY;
}


4. IntentService 


매번 쓰레드를 만들어서 작업을 처리한다면 여러 가지 작업을 동시에 처리할 수 있다는 이점도 있지만 개수가 많아짐에 따라 시스템 자원을 소모하게 되고 경우에 따라선 처리 시간은 중요하지 않고 작업이 순차적으로 처리하는 일이 필요한 경우 불필요한 오버헤드를 발생시키기도 한다. 차라리 이런 경우에는 여러 개의 쓰레드를 만들지 말고 하나의 쓰레드에서 작업을 하나씩 전달해주는 방식이 좋을 것 같기도 하다. 안드로이드에서는 이미 이런 경우를 고려해 IntentService 라는 클래스를 만들어 뒀다.


IntentService를 사용하면 메인 thread와 따로 도는 workqueue쓰레드가 생성되며 액티비티로부터 전달 받은 Intent를 순차적으로 아래의 onHandleIntent 함수에서 처리한다. 


protected void onHandleIntent(Intent intent)


onStartCommand 함수에서는 긴 작업을 별도의 쓰레드에서 처리했어야 했다면 IntentService의 onHandleIntent 함수안에서는 바로 길게 실행되는 작업을 둬도 상관 없다. 안드로이드가 알아서 작업을 처리해준다.

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

onSaveInstanceState  (0) 2019.07.15
JAVA의 static  (0) 2019.04.03
안드로이드 Service  (0) 2019.03.19
AsyncTask  (0) 2019.03.13
ViewPager와 PageAdapter  (0) 2019.03.05
px, dp, sp 개념 정리  (0) 2019.02.16

AsyncTask

컴퓨터공부/안드로이드 2019. 3. 13. 23:57 Posted by 아는 개발자

0. 소개


AsyncTask는 파일 다운로드 완료후 사용자에게 완료됐다는 Toast 메시지를 보내는 유스케이스처럼 특정 백그라운드 작업이 종료되고 사용자에게 화면으로 알림을 전달해야하는 경우 유용한 클래스다. 코드가 직관적이고 응용하기 쉬워 자주 사용되는 라이브러리지만 몇초 이내에 종료될 수 있는 작업인 경우에만 사용이 가능하며 그것보다 긴 작업은 다른 클래스를 사용해야 한다.


안드로이드 공식문서의 예제 코드를 통해 사용 방법을 익혀보자.


private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
     protected void onPreExecute(Integer... progress) {
         /* do nothing */
     }
     protected Long doInBackground(URL... urls) {
         int count = urls.length;
         long totalSize = 0;
         for (int i = 0; i < count; i++) {
             totalSize += Downloader.downloadFile(urls[i]);
             publishProgress((int) ((i / (float) count) * 100));
             // Escape early if cancel() is called
             if (isCancelled()) break;
         }
         return totalSize;
     }
     protected void onProgressUpdate(Integer... progress) {
         setProgressPercent(progress[0]);
     }
     protected void onPostExecute(Long result) {
         showDialog("Downloaded " + result + " bytes");
     }
 }



1. 상속 함수


클래스의 작업은 진행 순으로 크게 네가지로 구분된다.


protected void onPreExecute


이름에서 직감할 수 있듯이 AsyncTask 실행시 가장 먼저 불리는 함수다. 주로 여기에 백그라운드를 시작하기 전에 초기화해야하는 변수나 객체를 선언하는 용도로 사용한다.


protected Long doInBackground


백그라운드 작업이 실행되는 함수다. 예제 코드에서 확인 할 수 있듯이 여러 개의 인자를 전달받을 수 있으며 작업이 종료되면 특정 자료형의 결과 값을 리턴한다. 파라미터와 결과의 타입을 설정하는 방법은 다음 챕터에서 설명할 예정이다.


protected void onPostExecute

 

doInBackground  함수가 종료된 후 실행되는 함수다. doInBackground 함수의 결과값을 파라미터로 받으며 주로 예제 코드처럼 실행 완료메시지를 사용자에게 알림하는데 사용한다.


protected void onProgressUpdate


doInBackground 내에서 publishProgress 함수를 호출 할 때 불리는 함수이며 주로 작업의 진행 상황을 표시하는데 사용된다. 예제 코드에서는 다운로드 완료를 퍼센테이지로 전달했다.  



2. AsyncTask 인자


예제 코드의 DownloadFilesTask 클래스는 AsyncTask<URL, Integer, Long>을 상속하는데 여기서 부모클래스의 template 인자의 타입은 순서에 따라서 앞서 설명한 네가지 작업의 파라미터의 타입으로 매핑된다. 


첫번째 인자 (URL)


백그라운드로 실행하는 작업인 doInBackground 함수의 파라미터 타입이다. 주로 처리해야할 작업의 데이터를 전달하는 용도로 사용한다. 예제 코드에서는 URL 타입으로 설정해서 다운로드 받을 파일의 주소를 전달하는 용도로 사용하고 있다.


두번째 인자 (Integer)


진행 상황을 업데이트하는 onProgressUpdate 함수의 파라미터 타입이다. 예제 코드의 doInBackground 함수에서는 다운로드 진행 상황을 publishProgress 함수로 퍼센트로 전달하고 있으며 onProgressUpdate 함수는 Integer 형태로 받아서 화면에 표시하고 있다.


세번째 인자 (Long)


백그라운드 작업 종료후 실행하는 onPostExecute 함수의 파라미터 타입이다. doInBackground 함수의 리턴값이 onPostExecute 함수의 인자 값으로 전달되며 주로 작업의 정상종료 유무를 전달하기 위해 사용한다.



'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

JAVA의 static  (0) 2019.04.03
안드로이드 Service  (0) 2019.03.19
AsyncTask  (0) 2019.03.13
ViewPager와 PageAdapter  (0) 2019.03.05
px, dp, sp 개념 정리  (0) 2019.02.16
Device screen dpi 값에 따라 처리하기  (0) 2019.02.16

ViewPager와 PageAdapter

컴퓨터공부/안드로이드 2019. 3. 5. 23:38 Posted by 아는 개발자

0. ViewPager와 PagerAdapter


ViewPager와 PagerAdapter는 아래 네이버 앱의 뉴스, 연애 스포츠 탭처럼 큰 화면 안에서 여러 개의 작은 화면을 좌우로 움직이는 UI를 구성할 때 사용하는 안드로이드 라이브러리다. ViewPager는 TextView, Button과 같이 화면의 인터페이스로 사용자가 조작할 수 있는 컴포넌트고 PagerAdapter는 ViewPager가 생성한 공간을 채우는 데이터 공간으로 보면 된다. 


특별히 화면에 이펙트를 넣을 생각이 없으면 ViewPager는 안드로이드에서 제공하는 라이브러리를 그대로 쓰고 내가 넣고 싶은 화면을 담은 별도의 PagerAdapter를 만들어서 ViewPager에 연결 시키면 된다.


이버의 뉴스, 연예, 스포츠 탭은 ViewPager와 PagerAdapter로 만들었을 것 같다. (아닐수도 있고)


1. Fragment + PagerAdapter 


큰 화면의 단위는 Activity이고 작은 화면의 단위는 주로 Fragment를 사용하기 때문에 안드로이드에서는 PagerAdater를 Fragment를 사용해서 구현하기 쉽도록 FragmentPagerAdatper를 만들어 뒀다. 이것과 이름이 비슷한 FragmentStatePagerAdapter가 있는데 Fragment 메모리 관리를 어떤식으로 할 것인가에 따라서 조금 차이가 난다. 깊게 들어가면 어려울 수 있으니 간단하게 ViewPager가 관리하는 페이지가 얼마 되지 않는 경우에는 FragmentPagerAdapter를, 많으면 FragmentStatePagerAdapter를 사용하도록 하자. 이 둘로 나눴지만 상속해서 작성하게될 Custom FragmentPagerAdatper의 코드상의 차이는 거의 없다.


2. Custom FragmentPagerAdapter


앞서 말한대로 여러 개의 화면을 좌우로 슬라이드 할 수 있는 화면을 만들기 위해선 별도의 PageAdapter가 필요하다. 안드로이드에서 FragmentPagerAdapter 클래스를 만들어 뒀으니 이를 상속해서 Custom PageAdapter를 만들 수 있다. 몇가지 오버라이드 함수들만 살펴보자.


public Fragment getItem(int position)


n번째 포지션의 Fragment Item 오브젝트 정보를 리턴한다. 채울 화면의 순서를 변경하고 싶다면 여기서 return 하는 Fragment 객체를 position 에 따라서 변경하면 된다. 반드시 구현해야하는 함수다.

    @Override
    public Fragment getItem(int position) {
        Fragment tab = null;

        switch (position) {
        case 0:
            tab = new PreviewTab();
            break;
        case 1:
            tab = new CodeTab();
            break;
        }

        return tab;
    }


public int getCount()


ViewPager에 넣을 화면의 개수를 리턴하는 함수다. 이것도 반드시 구현해야 하는 함수다.

    @Override
    public int getCount() {
        return this.mNumOfTabs;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return mTitles[position];
    }


public Object instantiateItem


View에 넣을 Page 객체를 리턴해주는 작업. 이미 FragmentPagerAdapter에서 구현을 해뒀기 때문에 따로 오버라이드 할 필요는 없다. 커스텀한 fragment를 사용할 수도 있긴 할텐데 가능하면 부모 함수를 바로 쓰는 것을 추천한다.

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Fragment fragment = (Fragment) super.instantiateItem(container, position);
        registeredTabs.put(position, (IMarkdownTab)fragment);
        return fragment;
    }


public void destroyItem


생성한 Fragment를 제거하는 작업이다. instantiateItem 함수에서 부모 함수를 이용했다면 destroyItem 에서도 부모 함수를 이용해서 처리하는 것이 좋다. 그래야만 부모 함수의 변수들과 싱크를 맞출 수 있으니까. 마찬가지로 가능하면 부모 함수를 바로 쓰는 것을 추천한다.

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        super.destroyItem(container, position, object);
    }


전체 코드는 여기서 볼 수 있다.


3. ViewPager + Custom PageAdapter 


이제 만든 CustomPageAdapter를 xml의 ViewPager과 연결하면 된다. 아래와 같이 adapter 객체를 선언해준 후 ViewPager의 setAdapter 함수를 통해 연결 해줄 수 있다. 직관적인 코드라 이해하는데 별로 어렵지 않을 것 같다


adapter = new MarkdownPagerAdapter(getSupportFragmentManager(), Titles,
        selectedImgSrc, unselectedImgSrc, Titles.length);
viewPager = findViewById(R.id.view_pager);
viewPager.setAdapter(adapter);


4. ViewPager 페이지 선택 콜백함수


ViewPager 객체 내에 PageChange 리스너를 등록해서 페이지가 변경 될 때마다 특정한 작업을 하도록 만들 수 있다.


viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    }

    @Override
    public void onPageSelected(int curPosition) {
        IMarkdownTab curImt = adapter.getRegisteredTab(curPosition);
        IMarkdownTab preImt = adapter.getRegisteredTab(prePosition);

        if (curPosition == prePosition || curImt == null)
            return;

        if (preImt!=null)
            preImt.cbPageUnSelected();

        curImt.cbPageSelected();
        prePosition = curPosition;
    }

    @Override
    public void onPageScrollStateChanged(int state) {

    }
});


이또한 함수명으로 충분히 언제 불리는지 이해할 수 있을 것 같다. onPageSelected 함수에 넣은 코드는 사용자가 선택한 페이지의 화면을 새로고침 하도록 만든 작업이다.  상황에 맞춰서 커스터마이즈하면 된다. 아 그리고 리스너는 여러 개를 등록 할 수 있다. 난 이 점을 이용해 아래 그림처럼 화면 포커스를 바꿀 때 새로고침과 동시에 바의 위치와 그림을 바꾸기도 했다.      

     

   

리스너를 이용해 화면 포커스가 바뀔 때 그림 이미지와 바의 위치를 변경 할 수 있었다

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

안드로이드 Service  (0) 2019.03.19
AsyncTask  (0) 2019.03.13
ViewPager와 PageAdapter  (0) 2019.03.05
px, dp, sp 개념 정리  (0) 2019.02.16
Device screen dpi 값에 따라 처리하기  (0) 2019.02.16
안드로이드 선 추가하기  (0) 2019.01.26

px, dp, sp 개념 정리

컴퓨터공부/안드로이드 2019. 2. 16. 16:04 Posted by 아는 개발자

XML 파일 안에서 UI 컴포넌트의 크기를 정할 때 dp, sp, px 단위의 원리를 제대로 알지 않고 사용하는 경우가 있는데 이렇게 막 사용하다 보면 디바이스 해상도가 변경 될 때 화면이 깨지는 경우가 생긴다. 이번 포스트에서는 지금까지 막무가내로 사용해온(?) dp, sp, px 단위를 총 정리 해보려고 한다.


0. px (Pixel)



주소화 할 수 있는 화면의 가장 작은 단위. 점을 찍을 수 있는 가장 작은 단위라고 생각하면 된다. 화면 해상도와 관계 없이 컴포넌트의 크기를 절대값으로 세팅할 수 있으며 위의 그림으로 알 수 있듯이 오른쪽으로 갈 수록 폰의 해상도가 높아지면서 텍스트 상자와 이미지 아이콘이 작아진 것을 알 수 있는데 값을 픽셀 단위로 설정했기 때문에 그렇다. 이런 단위로는 여러 디바이스 환경마다 아이콘의 크기가 천차만별이 될 수 있어서 안드로이드 환경에서는 잘 사용하지 않는다.


1. dp (Density-independent Pixels)


화면의 해상도값에 따라서 크기가 조정되는 단위다. 160dpi 스크린 해상도에서는 1px만큼의 크기를 가지고 해상도 값이 커질수록 픽셀의 값이 커진다. 320dpi 해상도면 2px 만큼의 크기를 가지고 480dpi 면 3px만큼 가지는 원리다. 위의 그림에서도 보이듯이 해상도가 높아져도 텍스트상자와 이미지의 크기가 일정한 것을 알 수 있는데 이는 해상도에 따라서 크기가 재설정 됐기 때문이다. 해상도에 따라 크기가 자동으로 늘어나고 줄어들 수도 있으니 스펙이 다양한 안드로이드 디바이스 환경에서 적용할 때 유용한 단위다.


2.  sp (Scale-independent Pixels)


dp랑 비슷한 원리로 동작하며 사용자의 글짜 크기(font-size)를 조정할 때 사용되는 단위다. 

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

AsyncTask  (0) 2019.03.13
ViewPager와 PageAdapter  (0) 2019.03.05
px, dp, sp 개념 정리  (0) 2019.02.16
Device screen dpi 값에 따라 처리하기  (0) 2019.02.16
안드로이드 선 추가하기  (0) 2019.01.26
SharedPreference  (0) 2019.01.10

안드로이드 앱을 만들때면 무수히 많은 종류의 화면과 모두 호환 할 수 있는 UI를 만드는 일이 꽤나 골칫 거리다. 아이폰과 달리 안드로이드 폰은 제조사가 한군데만 있는 것도 아니고 또 같은 회사에서 만든 폰이라도 디스플레이 스펙이 제각각이라 이에대한 고려 없이 막만들었다간 화면이 깨져버리는 참사가 벌어지고 만다.


Android에서는 UI 개발자들이 이런 점들에 대해서 유연하게 대처 할 수 있도록 여러가지 API를 제공하는데 이번 포스트에서는 getResources().getDisplayMetrics().density 에 대해서 알아보려고 한다.


이 값은 디바이스의 160dpi 대비 디바이스의 screen density (dpi) 의 비율을 나타내며 실행되고 있는 디바이스 화면 스펙별로 값이 다르다. 디바이스의 화면이 320dpi면 2이고 420dpi면 2.625의 값을 가진다. 이 비율 값을 이용하면 다양한 하드웨어 스펙 별로 화면 값을 자동으로 조정 할 수 있게 된다.


int padding = (int) (TAB_IMAGE_VIEW_PADDING_PIV 
       / (getResources().getDisplayMetrics().density * 2));
imageView.setPadding(padding, padding, padding, padding);


이 값을 이용해 애플리케이션 오른쪽 상단 아이콘의 image view 주변 패딩 값을 조절하는데 사용 해봤다. View 클래스의 public void setPadding 함수는 인자를 절대 수치인 pixel 단위로 값을 받기 때문에 변환된 값을 전달해야한다. 그래서 getResources().getDisplayMetrics().density 값을 나눠서 화면 스크린 화질이 좋을 수록 값이 작아지도록 변경했다. 결과 아래 그림처럼 다른 화질을 가진 환경에서도 아이콘의 크기를 맞춰줄 수 있게 됐다.


'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

AsyncTask  (0) 2019.03.13
ViewPager와 PageAdapter  (0) 2019.03.05
px, dp, sp 개념 정리  (0) 2019.02.16
Device screen dpi 값에 따라 처리하기  (0) 2019.02.16
안드로이드 선 추가하기  (0) 2019.01.26
SharedPreference  (0) 2019.01.10