반응형

안녕하세요. 안드로이드에서 주로 사용하는 비동기 라이브러리중 하나인 Rx 에 대해서 설명 해보도록 하겠습니다.

안드로이드에서 어떤 서비스나 프로젝트를 만들때 네트워크 통신을 합니다. 그러면 자연스레 따라오는 비동기 네트워크 통신을 해야 합니다. 이유는 아래와 같습니다.

네트워크 통신은 메인 쓰레드에서 사용하지 말라는 Exception 리턴합니다.

어쩔수 없이 WorkThread 로 접근하여 네트워크 통신을 해야 합니다.

현재 Java 비동기를 자체적으로 지원해주는건 Callable, Runnable, ExecutorService .. 등이 있는데 이것들은 사실상 네트워크를 통해서 데이터를 가져오고 그걸 UiModel 에 맞게 가공하고 처리하는 과정을 간단하게 처리하는게 어렵습니다.

간단하게 Callable 과 Rx 을 사용 했을때 코드 스타일이 어떻게 다르고 어떤게 좋아 보이는지 간단한 예시로 보여드리겠습니다.

Callable Rx

Mapper 함수

 

어떤 타입이 더 나아보이시나요??

해당 예시는 정말 간단한 예시입니다.. 또한, 한가지 중요한 사실은 Callable 방식으로 사용하게 되면 "Mapper" 에 대한 처리는 반 강제적으로 UIThread 로 처리하게 되고, 이를 Worker Thread 로 하기가 좀 까다롭습니다. 이런 여러 이유 때문에 안드로이드에서는 아주 좋은 라이브러리들인 Rx, Coroutines 를 사용합니다.

그래서 이번시간에 흔히들 사용하는 Rx 에 대해서 설명해보도록 하겠습니다.

ReactiveX 란,

비동기 라이브러리중 하나로, 안드로이드에서만 존재하는게 아닌 RxJava, RxSwift 등등..여러 언어들을 지원하는 비동기 라이브러리 입니다.

Rx에는 크게 Single, Maybe, Flowable, Observable(이건 Rx2 부터 잘 안쓰고 Flowable 를 사용합니다.)

추가적으로 이 글에서 Observable 에 대한 설명은 안하도록 하겠습니다.  (굳이..할필요가..)

설명하면서 Rx의 버전은 v3.1.5 입니다.
Rx2 는 이미 업데이트 끝난지가 오래되서 굳이 거기에 맞춰서 설명할 필요는 없습니다.

 

Index

1. Single, Maybe, Flowable 간단 소개

2. 차가운 Observable, 뜨거운 Observable 간단 소개

3. 데이터 가공이나 유틸적인 부분을 처리할수 있는 함수들 몇가지만 소개

 

1. Single 

성공 또는 실패만 리턴하는 확실한 Class 주로 Network 통신할때 사용합니다. 그이유는 네트워크 통신은 성공 혹은 실패 둘중하나기 때문에 Single 과 아주 적합한 타입이기 때문에 사용됩니다.

간단한 예제로 설명 해드리면

- Just Type.

Single.just(System.currentTimeMillis())
            .subscribe(object : SingleObserver<Long> {
                override fun onSubscribe(d: Disposable) {
                    d.addTo(compositeDisposable)
                }

                override fun onSuccess(t: Long) {
                    Timber.d("onSuccess $t")
                }

                override fun onError(e: Throwable) {
                    Timber.d("onError $e")
                }
            })

보통은 subscribe (구독) 함수 호출할때 저렇게 안하고, Consumer 를 사용하는 함수 타입을 사용하는데 이해를 위해 SingleObserver Class 사용해서 처리했습니다. 

- Create Type

Single.create<Long> { emitter ->
            if (Random.nextBoolean()) {
                emitter.onSuccess(System.currentTimeMillis())
            } else {
                emitter.onError(RuntimeException("Sample Error"))
            }
        }.subscribe(object : Consumer<Long> {
            override fun accept(t: Long) {
                Timber.d("SUCC $t")
            }
        }, object : Consumer<Throwable> {
            override fun accept(t: Throwable) {
                Timber.d("ERROR")
            }
        }).addTo(compositeDisposable)

이번에는 Consumer 함수를 사용해서 표현해봤습니다.

위 방법과 같이 아주 간단하게 처리를 할수 있습니다. 하지만, 서비스에서는 절대 저렇게 간단하게 표현하는 곳은 아마..없을겁니다..

Maybe, Flowable 에 대해 간단한 설명이후 실제로 "데이터 스트림" 을 어떻게 가공하고, 병렬로 처리하는지 설명 하도록 하겠습니다. :)

 

2. Maybe

성공, 실패, 완료 이렇게 3가지 타입을 주는 애매한 Class 입니다.

- Just Type

Maybe.just(System.currentTimeMillis())
            .subscribe(object : Consumer<Long> {
                override fun accept(t: Long) {
                    Timber.d("SUCC $t")
                }
            }, object : Consumer<Throwable> {
                override fun accept(t: Throwable) {
                    Timber.d("ERROR")
                }
            }, object : Action {
                override fun run() {
                    Timber.d("onCompleted")
                }
            }).addTo(compositeDisposable)

- Create Type

        Maybe.create<Long> { emitter ->
            val ran = Random.nextInt(0 until 10)
            if (ran < 3) {
                emitter.onSuccess(System.currentTimeMillis())
            } else if (ran in 3..5) {
                emitter.onError(RuntimeException("Sample Error"))
            } else {
                emitter.onComplete()
            }
        }.subscribe(object : Consumer<Long> {
            override fun accept(t: Long) {
                Timber.d("SUCC $t")
            }
        }, object : Consumer<Throwable> {
            override fun accept(t: Throwable) {
                Timber.d("ERROR")
            }
        }, object : Action {
            override fun run() {
                Timber.d("onCompleted")
            }
        }).addTo(compositeDisposable)

 

위에서 말했다 싶이 "Success", "Error", "Completed" 이렇게 3가지 타입으로 리턴 할수 있습니다. 그러다보니 성공을 스킵하고 바로 완료로 넘겨버리는 아주 애매한 클래스라고 볼수 있겠습니다.

3. Flowable 

Observable Class 에서 "배압이슈" 로 인한 대응책으로 나온 클래스라고 보시면 되겠습니다. Single 의 상위버전? 이라고 생각하시면 되겠습니다. Single 보다 좀더 기능적으로 많은 Class 라고 생각 하시면 되겠습니다. 

배압 이슈가 머죠? 

쉽게 말해 몇초단위로 어떤 데이터를 계속 처리하는 구조에서 "subscribe" 안에 처리가 데이터를 가져오는 속도보다 느린경우 "배압" 이 생기게 됩니다. 그러다 보면 기존에 Observable 같은 경우는 정상적인 처리가 안됐습니다. 하지만, Rx 에서는 이를 처리하기 위해 몇가지 솔루션을 냈는데 그래서 나온게 Flowable 입니다. 

간단한 예제로 배압 이슈를 설명해드리겠습니다.

Flowable.interval(100,TimeUnit.MILLISECONDS)
            .onBackpressureBuffer()
            .subscribe({
               // 여기서의 처리가 500ms 걸리는 경우 배압 이슈가 생깁니다.
            },{
                
            }).addTo(compositeDisposable)

100ms 단위로 데이터를 방출합니다. subscribe 로 콜백 때립니다. 하지만 subscribe 에서의 처리가 500ms  걸리는 로직이라면 점점 데이터를 방출해야 하는데 쌓이겠죠?? 이걸 보고 배압 이슈라고 합니다. 

여기서는 설명하지는 않을건데 이 배압 이슈를 완벽히 Flowable 가 처리한건 아닙니다.  그냥 좀더 배압 이슈를 막을 솔루션을 내놨다 정도?

- Just Type

Flowable.just(System.currentTimeMillis())
            .subscribe(object : Consumer<Long> {
                override fun accept(t: Long) {
                    Timber.d("SUCC $t")
                }
            }, object : Consumer<Throwable> {
                override fun accept(t: Throwable) {
                    Timber.d("ERROR")
                }
            }, object : Action {
                override fun run() {
                    Timber.d("onCompleted")
                }
            }).addTo(compositeDisposable)

- Create Type

Flowable.create<Long>(object : FlowableOnSubscribe<Long> {
            override fun subscribe(emitter: FlowableEmitter<Long>) {
                val ran = Random.nextInt(0 until 10)
                if (ran < 3) {
                    emitter.onNext(System.currentTimeMillis())
                } else if (ran in 3..5) {
                    emitter.onError(RuntimeException("Sample Error"))
                } else {
                    emitter.onComplete()
                }
            }
        }, BackpressureStrategy.BUFFER)
            .subscribe(object : Consumer<Long> {
                override fun accept(t: Long) {
                    Timber.d("SUCC $t")
                }
            }, object : Consumer<Throwable> {
                override fun accept(t: Throwable) {
                    Timber.d("ERROR")
                }
            }, object : Action {
                override fun run() {
                    Timber.d("onCompleted")
                }
            }).addTo(compositeDisposable)

당연하겠지만, Single이나 Maybe, Flowable 함수 선언되는 방식은 비슷 비슷 합니다. ㅎㅎ Single 에서는 이게 되고 안되고.. 차이정도?

 

차가운 Observable? 뜨거운 Observable?

간단히 말해서 차가운 Observable 이라함은 subscribe 함수를 호출해야지만 데이터를 받을수 있다.

뜨거운은 subscribe 함수를 호출안하고 "onNext" 로 데이터를 방출하면 그냥 받아야 하는 곳에서 떄에 맞게 받을수 있고, Flowable 로 처리합니다. 

감이 안오신다고요? 몇가지 예시로 설명해보겠습니다. :)

- 차가운 Observable

apiService.fetchSingle()
            .map { toPayload(it) }
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({
                // API 요청하고 toPayload 로 데이터 가공할때까지 Cache Thread
                // Current UI Thread
                tvTitle.text = it.toString()
            }, {
                Timber.d("ERROR $it")
            }).addTo(compositeDisposable)

예..아까 위에 적었던 기본 예시중하나 맞습니다. 저게 차가운 Observable 입니다.

아..그래도 잘 모르시겠다고요? 한가지더 예시를 설명하겠습니다.

Flowable.interval(1000,TimeUnit.MILLISECONDS)
            .onBackpressureBuffer()
            .subscribe({
                // Do Working
            },{
                
            }).addTo(compositeDisposable)

그냥 단순하게 subscribe 를 해야지만 데이터를 받을수 있는것을 차가운 Observable 이라고 합니다. 

- 뜨거운 Observable

subscribe (구독) 을 받고 있는 곳과는 상관 없이 데이터 발행을 할수 있는 것 주로 "뒤로가기", "글로벌하게 회원 상태 변경시에 대한 UI 처리" 이럴때 많이 사용합니다.

크게 "subscribe" (구독) 이전 값을 받고 싶으면 Behavior, "subscribe" (구독) 이후 값을 받고 싶으면 Publish

몇가지 간단한 예제로 설명해드리겠습니다.

private val behavior = BehaviorProcessor.create<Long>()

// 데이터 받는 곳--1
behavior.subscribe({
    Timber.d("One Sub $it")
},{

})

// 데이터 방출!!
behavior.onNext(System.currentTimeMillis())

// 데이터 받는 곳--2
behavior.subscribe({
	Timber.d("Two Sub $it")
},{

})

구독이전 값을 받을수 있는 Behavior 를 사용했습니다. 저렇게 구성 했을때 One Sub 와 Two Sub 둘다 데이터를 받을수 있습니다. 

여러군데에 선언되어있어도 해당 클래스를 "onNext" 를 호출하면 subscribe 가 된곳 어디든 받을수 있습니다. like RxBusEvent..

private val publisher = PublishProcessor.create<Long>()

// 데이터 받는 곳--1
publisher.subscribe({
    Timber.d("One Sub $it")
},{

})

// 데이터 방출!!
publisher.onNext(System.currentTimeMillis())

// 데이터 받는 곳--2
publisher.subscribe({
	Timber.d("Two Sub $it")
},{

})

구독이후 값을 받을수 있는 Publish 를 사용했습니다. 저렇게 구성하면 One Sub 만 받고 Two 는 데이터를 받을수 없습니다. 왜냐면 데이터를 방출하는 시점이 Two Sub 보다 위에 있기 때문입니다. 

 

각 클래스별 간단히 설명을 해봤습니다.  그렇다면 이제 어떤 상황에서 어떤걸 사용하는게 가장 효율적일지 제 아주 주관적인 생각으로 설명해보겠습니다.

1.  로그인 성공이후 좋아요한 상품들을 보고싶어!

이 경우에서 중요한점은 로그인 성공 이후입니다. 그러니 "순차적" 으로 처리해야 합니다.

- 않좋은예

private fun exampleOne(){
        apiService.postLogin()
            .subscribe({
                // 로그인 성궁 후 좋아요한 상품 조회
                if(it.status) {
                    fetchUserLike()
                }
            },{
                
            }).addTo(compositeDisposable)
    }
    
private fun fetchUserLike(){
	apiService.fetchUserLike().subscribe({},{}).addTo(compositeDisposable)
}

 사실 이렇게 해도 되긴 되죠..상관은 없습니다만, Rx 에서 제공하는 함수들을 적극적으로 활용하지 못한 예입니다.

- 좋은 예

apiService.postLogin()
            .flatMap {
                if (it.status) {
                    apiService.fetchUserLike()
                } else {
                    throw NullPointerException("Login is Fail")
                }
            }.subscribe({ list ->
                // 좋아요한 상품들..
            }, {

            }).addTo(compositeDisposable)

Rx 에서 제공하는 flatMap 을 중간에 사용하면 되겠습니다. 그렇게 되면 로그인 성공했을때 "좋아요한 상품" 을 호출하면 그 이후에 대한 처리는 subscribe 에서 처리할수 있겠습니다.

 

2.  메인 탭에 탑배너, 하단배너, 날씨추천 상품 등등..을 병렬로 처리하고 싶어!

- 않좋은 예

    private fun start(){
        탑배너()
        하단배너()
        날씨추천상품()
    }

    private fun 탑배너(){
        Single.just("탑배너 API 호출해서 데이터에 추가합니다.")
            .subscribe({

            },{

            }).addTo(compositeDisposable)
    }

    private fun 하단배너(){
        Single.just("하단 배너 API 호출해서 데이터에 추가합니다.")
            .subscribe({

            },{

            }).addTo(compositeDisposable)
    }

    private fun 날씨추천상품(){
        Single.just("날씨추천상품 API 호출하여 데이터에 추가합니다.")
            .subscribe({

            },{

            }).addTo(compositeDisposable)
    }

이렇게 해도 상관은 없습니다만, 리스트 화면에 각 인덱스에 맞게 처리하기에는 한곳에서 처리 안하니까 버거워보이기도 하죠??

"merge" 를 이용한 방법

- 좋은예

private fun start() {
        Single.merge(탑배너(), 하단배너(), 날씨추천상품())
            .observeOn(AndroidSchedulers.mainThread())
            .buffer(3)
            .subscribe({ list ->
                // buffer 함수를 통해 호출한 데이터들을 한꺼번에 List 형식으로도 처리할수 있습니다.
            }, {

            }).addTo(compositeDisposable)
    }

    private fun 탑배너(): Single<String> {
        return Single.just("탑배너 API 호출해서 데이터에 추가합니다.").subscribeOn(Schedulers.io())
    }

    private fun 하단배너(): Single<String> {
        return Single.just("하단 배너 API 호출해서 데이터에 추가합니다.").subscribeOn(Schedulers.io())
    }

    private fun 날씨추천상품(): Single<String> {
        return Single.just("날씨추천상품 API 호출하여 데이터에 추가합니다.").subscribeOn(Schedulers.io())
    }

유의사항이라면 merger 를 사용할떄 스트림을 좀더 병렬적으로 처리하기위해 각 Single 마다 작업할 곳에 스케줄러를 따로 설정해줘야 합니다. 

이와 비슷한 mergeDelayError 도 있지만, 그거는 쉽게 말해서 "merge" 에서 병렬로 수행하다가 에러가 발생시 못받은 데이터들은 Dispose 되는 이슈가 있습니다. 그거에 대한 솔루션이라고 볼수 있겠습니다.

하지만, 제 경험상 스트림이 단순한 구조인경우 저게 제대로 되지만, 복잡한 스트림 구조들이라면 차리리 "onErrorReturn" 을 사용하는게 정신건강에 좋습니다. :)

 

아주 간단하게 flatMap, merge 에 대해서 설명해봤습니다. Rx 에서는 이것 말고도 진~짜 많은 기능?들을 지원합니다. 여러개의 "job" 들을 조합해서 새로운 데이터를 방출하는 zip, 특정 데이터들을 바꿔치기 하는 Flowable.switchMap.. 순서를 보장하는 concatMap 등등 진짜 많은 기능들을 지원하기 때문에 적제적소에 맞게 처리할수 있겠습니다. 

해당 포스팅에 관련된 자료는 제 깃허브에 올려놨습니다. 최대한 간단하게 예제들을 만들었으니 참고하시면 되겠습니다. :)

https://github.com/sieunju/rxSample

 

GitHub - sieunju/rxSample: RxJava 관련 설명 입니다다다

RxJava 관련 설명 입니다다다. Contribute to sieunju/rxSample development by creating an account on GitHub.

github.com

 

마지막으로 좀더 궁금한 사항이 있으면 댓글 남겨두시면 빠른시간내에 답변 달아주도록 하겠습니다.

반응형
  1. 은결. 2022.07.10 20:56 신고

    감사합니다!!!

반응형

요새 안드로이드에서 인기 있는 디자인 패턴이 MVVM 패턴이고 LiveData에 대해서 정확히 알아야 하는 클래스 입니다.

정의야 다른 블로그에 글들이 많으니까 배제하고

실제로 적용해보고 어떤 문제가 있고 어떻게 풀어나가는지..
그리고 몇가지 유의사항이 있는데 그런 이슈는 어떻게 풀어내야 하는지 알아보도록 하겠습니다. 🤔

  1. LiveData, MutableLiveData 차이
  2. LiveData 의 UI 업데이트는 언제 실행 하는지
  3. RecyclerView -> ViewHolder 에서 ViewModel 의 LiveData 를 어떻게 처리하는지? 또한 이러한 구조는 지양 해야 하는지?
  4. List 형식을 LiveData 에서 처리하는 방법
  5. Fragment in Fragment 구조에서 LiveData 사용시 유의 사항
  6. 끝으로
  7. 참고 문헌

 

1. LiveData, MutableLiveData 차이

LiveData 외부에서 데이터 읽기만 가능합니다.
setValue or postValue 를 protected 로 보호 했기 때문에 읽기만 가능합니다.
MutableLiveData 외부에서 데이터 읽기/쓰기가 가능합니다.

- 여담이지만, 안드로이드도 코틀린처럼 네이밍을 맞추려는거 같습니다. Like List (읽기만 가능) MutableList (읽/쓰기 가능)

  • postValue 함수란?
    • WorkThread, UI Thread 둘다 사용가능한 함수 입니다. 
    • 해당 함수를 호출하게 되면 아래 캡처처럼 데이터를 메인 쓰레드로 보냅니다.

  • setValue 함수란?
    • UI Thread 에서만 동작 가능한 함수입니다.
  • 실제로 사용하면 알겠지만, UI 의 상태를 표시할때 LiveData 를 사용합니다. (다른곳에서 사용할 이유는..있을리가..없습니다.)
    • 웬만하면 postValue 는 지양하는것이 개인적으로 좋습니다. 그 이유로 간단한 화면같은 경우에는 맘대로 써도 되지만 조금 복잡한 화면에서는 같은 LiveData 를 여러 군대에서 사용하는 경우 데이터가 생각대로 원하는 값이 업데이트 안될때가 있습니다. 
    • UI 화면 업데이트는 확실하게 setValue 를 통해서 처리하는걸 지향 합니다. 
    • 아래 예제 처럼 실제로 저렇게 하지는 않지만, "postValue" 를 습관적으로 사용하는 경우 엄청난 이슈를 야기 할 수 있습니다.
      실제로 디버그 모드에서는 저 조건문에 들어갈수 있더라도, 실제 프로덕션 모드에서는 99.99% 저 조건문은 안들어 갑니다.
      (이유는 프로덕션 모드에서는 난독화 및 실행하는 속도가 빠르기 때문입니다.)
    private val _testTitle: MutableLiveData<String> by lazy { MutableLiveData() }
    val testTitle: LiveData<String> get() = _testTitle
    
    fun doWork(){
    	// 300ms 이후 _testTitle 에 값을 입력합니다.
        _testTitle.postValue("Hellow")
        if(testTitle.value = "Hellow") {
        	// 데이터가 확실하지 않습니다.
        }
    }

 

2. LiveData 의 UI 업데이트는 언제 실행 하는지

  • LiveData 는 기본적으로 LifecycleOwner 를 의존합니다. 
    • 데이터를 postValue or setValue 를 호출하면 해당 LiveData 에서는 데이터를 가지고 있습니다.
    • Activity 에서 onStart, onResume 상태일때 "observe" 함수를 호출한 곳에서 콜백을 받습니다. 
    • 또한, Lifecycle 에서 onDestroy 상태일때 알아서 "observe" 호출한 리스너를 제거 합니다. 
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
            Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
            if (currentState == DESTROYED) {
            	// 여기서 제거합니다!
                removeObserver(mObserver);
                return;
            }
            Lifecycle.State prevState = null;
            while (prevState != currentState) {
                prevState = currentState;
                activeStateChanged(shouldBeActive());
                currentState = mOwner.getLifecycle().getCurrentState();
            }
        }

 

3. RecyclerView -> ViewHolder 에서 ViewModel 의 LiveData 를 어떻게 처리하는지? 또한 이러한 구조는 지양 해야 하는지?

  • RecyclerView -> ViewHolder 에서 ViewModel 에 있는 LiveData를 Observer 를 합니다.
ViewHolder.kt

init {
	binding.setVariable(BR.vm, viewModel)
    itemView.doOnAttach {
    	binding.lifecycleOwner = ViewTreeLifecycleOwner.get(it)
    }

    itemView.doOnDetach {
    	binding.lifecycleOwner = null
    }
}

ViewHolder.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="vm"
            type="com.hmju.livedata.testrecyclerview.TestRecyclerViewModel" />

        <variable
            name="model"
            type="com.hmju.livedata.testrecyclerview.TestModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="300dp">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:padding="15dp"
            android:text="@{model.title}"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:padding="15dp"
            android:text="@{vm.rvTitle}"
            app:layout_constraintBottom_toBottomOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

ViewModel.kt
val ranArr = listOf<String>("가","나","다","라","마","바","사")
viewModelScope.launch(Dispatchers.Main) {
	repeat(100) {
        _rvTitle.value = ranArr[Random.nextInt(ranArr.size)
        delay(1500)
    }
}
  • 1.5 초 단위로 랜덤으로 ViewModel 에서 텍스트 값을 변경하는 처리를 하고 ViewHolder에서는 이를 받아서 "setText" 한다고 쳤을때 binding.lifecycleOwner 를 적절한 위치에 추가 하고 적절한 위치에 메모리 해제를 합니다.
  • 실행한 영상입니다.
    •  
  • 이렇게 간단한 구조에서는 잘 됩니다. 따지고 보면 ViewHolder 에서도 LifecycleOwner 를 binding 에 잘 집어 넣어주면 정상적으로 처리 됩니다...만, 지양하는 이유갸 있습니다. 
    • RecyclerView 는 화면에 보여지는 부분과 약간의 여유가 있는 부분 까지 onBindView 함수를 호출하여 데이터를 갱신 합니다.
    • 즉, LiveData 에서 데이터를 갱신 했지만,  업데이트할 ViewHolder 가 onDetach 상태를 한동안 있다가 다시 onAttach 상태로 전환 되는 경우 제대로 업데이트가 안될수도 있습니다. (경험담..)
      ex.) RecyclerView in RecyclerView in Tab 처럼 보이는 ViewHolder 에서 탭 인디게이터 처리를 "Pull On Refresh"  할때 제대로 이루어지지 않는 경우가 있었습니다. 
    • 제 개인적인 생각이지만, ViewHolder 에서 데이터 갱신처리는 notifyItemChange or DiffUtil 를 사용하는게 정신건강에 좋습니다..

4. List 형식을 LiveData 에서 처리하는 방법

  • LiveData 에서 List 형식은 무조건 사용합니다.. 목록 화면을 만든다면 무조건 사용해야 합니다. 일반적으로 아래와 같이 구성이 될겁니다.
  •  
    private val _listTest : MutableLiveData<MutableList<String>> by lazy { MutableLiveData() }
    val listTest : LiveData<MutableList<String>> get() = _listTest
  • 여기서 List 를 addAll 를 해서 처리 했을때 제대로 동작이 안하는 이슈가 있습니다. 
  • 보통 stackOverFlow 에서는 아래와 같이 솔루션을 줍니다.
fun <T> MutableLiveData<T>.notifyObserver() {
    this.value = this.value
}
굳이..저걸 모든 List 형식의 LiveData 에다가 처리를 해야 할까??? 이런 딜레마에 빠지게 됩니다.
그럼 AddAll 은 그렇다 쳐도 AddIndex, remove, removeInstance 이런 기능들에 대한 처리가 엄청 애매하게 됩니다.

 

  • 다른 개발자들은 모르겠지만, 저 같은 경우에는 쓸데 없이 반복적인 함수를 싫어 합니다.
    예를 들어 아래와 같이 LiveData.value.forEach.... 이렇게 뎁스가 많아지는 케이스 와 List 를 가지고 여러 처리를 외부에서 다 드러내서 처리하는걸 지양 합니다.
        _listTest.value?.forEach { 
            if(it == "ListLiveData?") {
                // do something...
            }
        }
  • 저같은 경우에는 ListLiveData 를 따로 만들어서 처리합니다.
class ListLiveData<T> : MutableLiveData<MutableList<T>>() {
    private val temp: MutableList<T> by lazy { mutableListOf() }

    init {
        value = temp
    }

    override fun getValue() = super.getValue()!!
    val size: Int get() = value.size
    operator fun get(idx: Int) =
        if (size > idx) value[idx] else throw ArrayIndexOutOfBoundsException("Index $idx Size $size")

    fun data(): List<T> {
        return temp
    }

    fun add(item: T) {
        temp.add(item)
        value = temp
    }

    fun addAll(list: List<T>) {
        temp.addAll(list)
        value = temp
    }

    fun clear() {
        temp.clear()
        value = temp
    }
}
  • 위 클래스는 아주 간단한 함수들만 추가 했지만, List 에서 여러 "기능" 들에 대해서는 사용자 입맛에 맞게 만들면 되겠습니다. 

 

5. Fragment 에서 LiveData 사용시 유의 사항

  • 여기가 가장 중요한 부분입니다. 위에 있는 글을 읽고 LiveData 가 사용자에게 보여지는 시점에 "observer" 함수가 선언된 위치에 "최신값" 을 콜백처리합니다.
    위 내용에는 빠뜨렸지만,  Bidning -> Activity or Fragment 에서도 setLifecycleOwner 를 하면서 Layout 안에 LiveData 를 받아서 처리하는 부분이 제대로 업데이트 되도록 합니다.
binding = DataBindingUtil.setContentView<ATestRecyclerViewBinding>(
            this,
            R.layout.a_test_recycler_view
        ).apply {
        // DataBinding 에서의 "setLifecycleOwner" 처리입니다.
       lifecycleOwner = this@TestRecyclerViewActivity
       setVariable(BR.vm,viewModel)
}
  • 하지만,Lifecycle 이 복잡한 Fragment 에서는 몇가지 유의해야 하는 상황이 있습니다.
    • 하나의 액티비티 안에 여러개의 Fragment 를 onCreateView, onDestoryView 를 처리하면서 LiveData 의 고유 특성 때문에 조심해야 하는 부분이 있습니다. 흔히 자주 사용하는(?) Fragment Navigation 을 예시로 보여드리겠습니다.
     

  • 위와 같은 구조에서 ViewModel 를 통해 버튼 클릭 이벤트를 처리한다면..? 어떻게 될까요??
  •  
ViewModel.kt
class SieunViewModel : ViewModel() {

    val startQtzzFragmentEvent: MutableLiveData<Unit> by lazy { MutableLiveData() }

    fun onMoveFragment() {
        startQtzzFragmentEvent.value = null
    }
}

Fragment.kt -> onViewCreated
with(viewModel) {
	startQtzzFragmentEvent.observe(viewLifecycleOwner) {
    	findNavController().navigate(R.id.qtzzFragment)
    }
}
  • 위와 같이 처리하고 나서 SieunFragment -> QtzzFragment 로 전환후 "뒤로가기" 를 실행하는 경우 SieunFragment 로 절대 가지 못합니다..그 이유는 이전에 있던 Fragment 에 선언된 ViewModel 에 LiveData 에 데이터가 남아있기 때문입니다.
  • Fragment Navigation 특성상 이전에 있던 Fragment를 제대로 Destory 하지 않기 때문에 ViewModel 객체들은 어딘가에 남아 있습니다. 그러다 보니 뒤로가기를 선택하여 이전 Fragment 로 넘어가는 경우 onDestoryView -> onCreateView -> onViewCreated 상태로 넘어가면서 "observe" 로 함수를 다시 받아서 해당 LiveData 의 최신값을 다시 받게 됩니다. 
  •  

LiveData 는 onViewCreated 에 리스너를 달아서 처리하기때문에 Fragment A, B가 무한 반복 됩니다.

  • 아주 간단한 해결책으로는 xml 에서 onClick 할때 처리를 Fragment 자체적으로 처리하는 방법이 있습니다.
    fun onMoveAppleFragment(){
        findNavController().navigate(R.id.appleFragment)
    }
  • 이럴듯 ViewModel 은 오랫동안 데이터를 가지고 있어서 장점이 있지만, 또한 단점아닌 단점이 될수도 있습니다. LiveData 는 최신의 데이터를 onResumed 상태일때 콜백을 무조건 전달하기 때문에 위와 같은 경우에는 단점이 될수 있습니다.
  • 사실 위와 같은 이슈는 Fragment Navigation 안쓰고 자체 FragmentManager 를 사용하면 AKA. replace, addBackStack을 적절히 사용하면 해결할수 있겠습니다.

6. 끝으로

  • LiveData 는 Activity Lifecycle 에 의존하고 화면이 보여지는 상태(onResume) 일때만 "observer" 함수가 선언된 곳에 콜백한다.
  • MutableLiveData 를 잘 활용하여 ListLiveData, SingleLiveEvent 이런 여러 커스텀한 클래스를 만들수 있다.
  • LiveData 는 항상 최신값을 계속 저장하고 있습니다. "observer" 함수에 호출했다고 해서 그 뒤로는 같은값이 호출 안되는건 아닙니다.
  • LiveData 에서 콜백을 받고 싶다면 setLIfecycleOwner 를 무조건 해야 한다.

 

7. 참고 문헌

https://pluu.github.io/blog/android/2020/01/25/android-fragment-lifecycle/

 

Pluu Dev - Fragment Lifecycle과 LiveData

[요약] Performance best practices for Jetpack Compose (Google I/O '22) Posted on 24 Jun 2022 [요약] Lazy layouts in Compose (Google I/O '22) Posted on 19 Jun 2022 [발표 자료] Modern Android Developer Posted on 14 Jun 2022 [발표 자료] Whats new

pluu.github.io

관련 코드는 아래 링크 클릭하면 되겠습니다.

https://github.com/sieunju/liveDataSample

 

GitHub - sieunju/liveDataSample: LiveData 샘플 예제입니다.

LiveData 샘플 예제입니다. Contribute to sieunju/liveDataSample development by creating an account on GitHub.

github.com

 

제가 만든 안드로이드 개발자라면 꼭 겪는 딥링크 테스트에 필요한 앱소개입니다. 많은 애용 바랍니다 :)

https://jsieun73.tistory.com/185

 

[안드로이드] 딥링크 테스트 어플 소개

안녕하세요. 이번에는 제가 딥링크 테스트를 하고 있다가 너무 귀차니즘이 발동하여 앱을 하나 만들었는데 한번 소개해볼까 합니다. https://play.google.com/store/apps/details?id=com.hmju.deeplink 퀵딥링크(Q.

jsieun73.tistory.com

 

반응형
  1. 은결. 2022.06.27 02:46 신고

    List 형식을 LiveData 에서 처리하는 방법 완전 신세계네요 ㄷㄷ 많이 배우고 갑니다!! 👍🏻👍🏻

반응형

안녕하세요. 이번에는 제가 딥링크 테스트를 하고 있다가 너무 귀차니즘이 발동하여 앱을 하나 만들었는데 한번 소개해볼까 합니다. 

https://play.google.com/store/apps/details?id=com.hmju.deeplink 

 

퀵딥링크(Quick DeepLink) - Google Play 앱

딥링크 테스트를 아주 간단하게 도와주는 앱입니다.

play.google.com

 

안드로이드 개발하면서 거의 웬만한 서비스에서 "딥링크" 를 많이 사용합니다. 딥링크 관련 테스트를 할 때 몇 가지 방법들이 있습니다. 

1. 명령어를 쳐서 테스트 하는 방법

https://developer.android.com/training/app-links/deep-linking?hl=ko#testing-filters 

 

앱 콘텐츠 딥 링크 만들기  |  Android 개발자  |  Android Developers

사용자가 링크에서 앱에 진입할 수 있도록 하려면 관련 활동의 인텐트 필터를 앱 manifest에 추가해야 합니다. 이러한 인텐트 필터는 모든 활동의 콘텐츠로 연결되는 딥 링크를 허용…

developer.android.com

( 이 방법이 정석이라고 하지만, 정말 귀찮고 테스트할 때마다 링크들을 타이핑이나 복/붙해서 처리해야 합니다.)

2.. html 파일을 만들어서 <a> 링크를 이용해서 처리합니다.

또 여러 방법이 있겠지만 모두 직접 타이핑을 하거나 링크를 만들기까지 여러 부수적은 귀찮음이 많아집니다. 

그래서 플레이 스토어에 이런 고민에 대해서 여러 개발자들이 만든것들이 몇 개 있긴 하지만, 직접 타이핑을 치거나 좀 불편한 방식으로 앱이 있었습니다. 

아주 간단하게 테스트를 할수 있는 앱을 만들어봤습니다.

제가 만든 앱에서 테스트 하는 방법은 아래와 같습니다.

  1. PC에서 QR 코드 생성기 페이지에 들어가서 (https://ko.online-qrcode-generator.com/) 테스트할 링크를 입력합니다.
  2. 앱에 QR 스캐너 페이지에 진입하여 생성된 QR 코드를 생성하면 자동으로 인식하여 추가할 건지 물어봅니다. 
  3. 추가하면 목록페이지에 아이템 하나가 생성이 되고 선택하면 타겟팅한 앱이 실행됩니다. 
  4. 테스트할 링크들을 위 과정대로 반복하시고 나면 나중에 테스트 할때 손쉽게 테스트할 수 있겠습니다. 
참고로 링크가 추가되면 해당 링크는 로컬에서만 저장되기 때문에 보안 이슈에 대해 걱정안하셔도 됩니다. :)

간단한 사용법 영상입니다. 

 

여기에 추가 하고 싶은 아이디어에 대해서 댓글 남겨주시면 시간 날 때 한번 추가해보도록 하겠습니다. 

추가 기능

- 목록 순서를 간단하게 변경할수 있는 UX 구현

- 스캐너 페이지에서 여러개 스캔후 한꺼번에 추가 할수 있는 기능 구현

-  실험실 메뉴 추가 (현재 구현후 배포된 상태입니다.)

 

그럼 많은 이용 바라고 널리 널리 퍼뜨려 주세요~ 

반응형
반응형

안녕하세요 오늘 개인 나스에서 머좀 찾아보다가 예전 학부시절 졸업 작품으로 게임 프로젝트를 했던 자료가 있더군요..

정말 맨땅에 해딩하는 식으로 팀원들과 무언가를 만들었던 시절..

학교에선 "게임" 이라는 카테고리라는 이유만으로 지원을 못받았지만 팀원들끼리 으쌰으쌰 돈 모아서 캐릭터랑 오브젝트 등..샀던 그시절...

지금은 잘지내고 있는지 ㅋㅋㅋ 물론 다들 게임쪽으로는 전혀 안가고 다른 포지션 개발자로 갔다는게 현실..(게임 개발자가 참...그렇슴다)

각설하고 그때 제작해둔 영상 보여드립니다ㅋㅋㅋ (아마 나중에 올린걸 후회할수도 있는데 그거 또한 아름다운 추억이...)

근데 보면서 정말 닭살이 돋습니다 ㅋㅋㅋㅋ

 

1. 최종 졸업작품 영상

 

반응형

2. 중간 졸업작품 영상

 

3. 서버 & 유니티 연동 영상

 

4. 서버 과부화 테스트 영상

 

 

감사합니다.

 

반응형
반응형

안녕하세요 이번 글에서는 안드로이드 앱을 직접 운영하는 사람들에게만 필요한 포스팅이 되겠습니다. 

앱을 운영 하다 보면 기능, 버그 들이나 수정사항이 발생하면 앱을 '배포' 할 때가 있습니다. 손수 배포하는 과정은 아래와 같습니다.

  1. 배포할 내역들을 한 브렌치 (develop or master) 에 머지합니다. 
  2. 릴리즈 모드로 .aab or .apk 버전을 생성합니다. aka 배포 버전 만다.
    (구글에서 작년부터 aab 를 적극 지향하기 때문에 이제는. apk는... 보내줘야 합니다..ㅠㅠ)
  3. 플레이 스토어 앱 콘솔에 들어갑니다. 
  4. 배포할 앱 선택 
  5. 파일 업로드 및 변경사항 적기
  6. 검수 요청 
증말 복잡하죠.. 일단 배포 버전 파일을 말고 나서 콘솔에 들어가고~ 변경사항도 적고.... 개발자들은 정말 귀찮고 반복적인 작업을 아주 싫어합니다.

 

그래서 이번에 Github Action 이랑 라이브러리를 활용하여 특정 브렌치에 푸시하면 자동 배포되도록 하는 방법에 대해서 알려드리겠습니다. 

우선 준비물과 조건이 필요합니다. 

  1. 앱을 한번이라도 배포를 한 적이 있어야 합니다. 
  2. Gradle Play Publisher 라이브러리를 프로젝트 Application Module 추가해야 합니다.

https://github.com/Triple-T/gradle-play-publisher#quickstart-guide

 

GitHub - Triple-T/gradle-play-publisher: GPP is Android's unofficial release automation Gradle Plugin. It can do anything from b

GPP is Android's unofficial release automation Gradle Plugin. It can do anything from building, uploading, and then promoting your App Bundle or APK to publishing app listings and other metadat...

github.com

 

 

저 같은 경우에는 개인적으로 스토어에 올린 앱이 있어서 그걸로 정~말 여러가지 들을 해봤습니다. ㅠㅠ 수많은 실패로 얻은 경험이니 보는 사람은 이런 실패들을 겪지 않았으면 좋겠습니다 ㅎㅎ

수많은 실패 끝에 결국 성공할때 이 쾌감...🤩

 

저 같은 경우는 저장 소안에 keystore.jks 가 없고 깃허브 액션 Secrets 에 baase64로 변환하여 저장했습니다. 그 이유에 대해서는 오픈화된 저장소에 대해서 운영할 수도 있어서 한번 아래 화면처럼 저장해서 사용 중에 있습니다.

 

이제 각설하고 설정방법에 대해서 알려드리겠습니다. 
반응형

Google Play Store & Google Cloud Platform API 설정

1. 구글 클라우드 플랫폼 콘솔에 들어갑니다. https://console.cloud.google.com/?hl=ko

 

Google 클라우드 플랫폼

로그인 Google 클라우드 플랫폼으로 이동

accounts.google.com

2. 프로젝트를 생성합니다. 

3. 생성후 좌측 메뉴에 "API 및 서비스" > "사용자 인증 정보" 선택 후 "서비스 계정 관리" 선택하여 서비스 계정을 생성합니다.
여기서 중요한건 서비스 계정 *** iam.gserviceaccount.com  복사합니다.  나중에 Google Play Console에서 사용합니다. 

간단하게 입력후 짜잔~만들어집니다.

4. 서비스 계정을 선택후 "키" 탭 선택 후 키를 추가합니다.

5. 그럼 자연스레 ".json" 파일이 다운로드하게 됩니다. 해당 파일은 꼭 간직했다가 "안드로이드 프로젝트" 폴더 아무 데나 옮깁니다.
 ex.) 저같은 경우에는 릴리즈 노트 경로가 고정이라 거기에 저장했습니다. -> app/src/main/play/googlecloudplatform.json 

6. 자 이제 Google Play Platform에서 처리할 거는 다했습니다. 이제 Google Play Console에서 작업해야 할 것들이 있습니다. 

7. 플레이 콘솔 메인에 진입합니다. https://play.google.com/console/about/

 

Google Play Console | Google Play Console

앱 및 게임이 성장할 수 있도록 사용자에게 도달하고 사용자 참여를 유도하는 데 도움이 될 도구, 프로그램, 통계를 이용하세요.

play.google.com

8. 좌측 메뉴에 "설정" > "API 액세스" 진입합니다. 저 같은 경우에는 이미 Google Cloud 프로젝트와 연결이 되어 있어서 아래 화면처럼 되어있는데 처음에는 새 프로젝트를 연결할 건지, 기존 프로젝트를 연결할 건지 선택하는 화면이 나옵니다.
여기서 중요한점은 PlayConsole 관리자 계정과 Google Cloud Platform 계정이 동일해야 합니다. 그래야 기존 프로젝트에서 연결이 됩니다. 

9. Play Android Developer API 활성화합니다. 

10. 서비스 계정 탭에 보시면 여러 이메일이 있는데 아까 서비스 계정 추가했던 "3번" 여기서 중요한건 ***iam.gserviceaccount.com 계정을 찾은 후 "권한 부여" 선택 후 자동 배포하고 싶은 앱을 선택하고 "사용자  초대" 선택합니다. 
이후 계정 권한에 들어가서 출시에 대한 권한만 허용합니다. 

출시 권한만 허용 해놓고 저장합니다.

11. 이제 Google Coud Platform, Google Play Console에 해야 할 것들은 모두 마쳤습니다.

 

Android Studio & Github Action 설정 방법

  1. 안드로이드 스튜디오 Application Module에 triplet 플러그인을 추가합니다.
plugins {
    id("com.android.application")
    id("com.github.triplet.play") version "3.7.0"
}

그리고 'sync now' 선택한 뒤에 Application Module에 앱 배포 시에 대한 설정을 합니다.

play {
    // production -> 상용, internal -> 내부, alpha, beta
    defaultToAppBundles.set(true)
    track.set("production")
    // track.set("internal")
    userFraction.set(0.3)
    releaseStatus.set(ReleaseStatus.IN_PROGRESS)
    serviceAccountCredentials.set(file("$projectDir/GoogleCloudPlatform.json"))
}

위 코드 안에 "GoogleCloudPlatform.json" 은 위에서 서비스 계정에서 키 추가로 받은. json 파일입니다. 원하는 경로에 추가하시면 되겠습니다. 

2. 릴리즈 노트 파일을 설정해야 합니다. 해당 경로는 triplet.play 라이브러에서 고정되어 있기 때문에 아래와 같습니다.

(Application Module) > src/main/play/release-notes/ko-KR/default.txt 

알파 버전 배포도 하고 싶다면 alpha.txt 를 생성하여 출시 내용을 적으시면 되겠습니다. 

예시 

[v.1.0.0]

· 실험실 메뉴를 만들었어요. 메인 > 우측 상단 [...] 선택
· WIFI 스캔 지원하는 기능을 만들었어요.
· 여러분들의 소중한 의견을 반영하여 안정성을 향상하고 소소한 버그를 수정했어요.

많이 이용해주세요 😀

 

3. signingConfigs 처리 방법

Application Module
android {
	signingConfigs {
        create("release") {
        	// Github Action 에서 키스토어를 만드는 경우
            val files = file("/home/runner/work/_temp/keystore/").listFiles()
            if (files != null) {
                storeFile = files.first()
                storePassword = System.getenv("Actions secrets 에 설정한 값")
                keyAlias = System.getenv("Actions secrets 에 설정한 값")
                keyPassword = System.getenv("Actions secrets 에 설정한 값")
            }
            
            // 저장소에 직접 키스토어를 저장하는 경우
           	storeFile = file("키스토어 경로")
            storePassword, keyAlias, keyPassword 적제적소에 맞게 설정하면됩니다.
           
        }
    }
}

 

4. 릴리즈 Task 설정

Application Module

tasks.register("release") {
    dependsOn(tasks["clean"])
    dependsOn(tasks["bundleRelease"])
    mustRunAfter(tasks["clean"])
}

 

5. 안드로이드 스튜디오에서의 설정은 모두 마쳤습니다. 이제 끝으로 Github Action 및 keystore 를 base64 로 변환 후
"Actions secrets" 추가하는 방법에 대해 알려드리겠습니다. 

우선 맥을 사용하신다면 아래 명령어 양식대로 쳐서 .txt 파일을 생성합니다.

openssl base64 -in {키스토어}.jks -out {변환할 파일 명}.txt

6.  깃허브 저장소에 들어가서 "Settings" > 좌측 메뉴에 "Secrets" > "Actions" 들어가서 Github Action 에서 필요한 키스토어 정보를 추가합니다. 키스토어 파일을 base64 (문자열) 로 변환한 것 또한 전체 복사하여 알맞은 이름값과 내용 값을 입력합니다.

 7. 자 이제 모든 게 끝났습니다. 이제 yml 만 작성하면 되겠습니다. 

name: Build and upload release aab

on:
  push:
    branches: [ master ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
        with:
          ref: master

      # JDK 설정
      - name: set up JDK 11
        uses: actions/setup-java@v2
        with:
          java-version: '11'
          distribution: 'adopt'
          check-latest: true
	# secrets.APP_RELEASE_KEY_STORE_BASE_64 -> 키스토어를 문자열(base64) 인코딩한 이름값 #
      - name: Decode Keystore
        id: decode_keystore
        uses: timheuer/base64-to-file@v1
        with:
          fileName: '/keystore/키스토어를 저장할 이름.jks'
          encodedString: ${{secrets.APP_RELEASE_KEY_STORE_BASE_64}}
	
      - name: Make gradlew executable
        run: chmod +x ./gradlew
	
    # Action Secrets 에 키스토어에 필요한 정보들을 입력한 내용을 env 로 저장합니다. #
    # 프로젝트 파일에서 'System.getenv' 로 가져다 사용할수 있습니다. #
      # 앱 AAB 및 배포
      - name: Build Release And Publish AAB
        run: ./gradlew publishReleaseBundle
        env:
          SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
          SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
          SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
	
    # 배포한 흔적을 남기고 싶다면 아래 스크립트들을 입력하고 아니면 위에 까지만 하면 되겠습니다. #
      # 버전 정보 가져오기
      - name: Get version
        id: get_version
        run: |
          echo "::set-output name=code::$(grep versionCode buildSrc/src/main/java/Dependencies.kt | awk '{print $5}')"
          echo "::set-output name=name::$(grep versionName buildSrc/src/main/java/Dependencies.kt | awk '{print $5}' | tr '"' ' ' | tr -d " ")"
      # 태그 이름 가져오기
      - name: Get tag name
        id: get_tag
        run: echo "::set-output name=name::v${{ steps.get_version.outputs.name }}"

      # 릴리즈 이름 가져오기
      - name: Get release name
        id: get_release
        run: echo "::set-output name=name::v${{ steps.get_version.outputs.name }}"

      # 배포 내역 가져오기
      - name: Get Release Note
        id: get_note
        run: echo "::set-output name=name::v${{ steps.get_version.outputs.name }}"

      # 릴리즈 생성
      - name: Generate Release
        uses: actions/create-release@latest
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          release_name: ${{ steps.get_release.outputs.name }}
          tag_name: ${{ steps.get_tag.outputs.name }}
          draft: false
          prerelease: false

8. # ~ # 로 중요한 스크립트에 설명을 남겼습니다. 저 스크립트는 "master" 에 푸시를 하면 자동 배포되도록 처리했습니다. 

9. 성공 화면 ( 앱 출시 형태는 "수동 배포" 입니다. )

 

짜잔~ 모든 게 끝났습니다. 이제부터는 앱 배포까지 귀찮은 일은 GithubAction 한테 맡기도록 합시다!!

끝으로 제가 최근에 안드로이드 개발자라면 꼭 필요한 테스트인 딥링크 테스트를 간단히 할 수 있는 앱을 소개하겠습니다. 
(사실 홍보 목적이었습니다.. 🤩)

 

딥링크 테스트할 때 테스트할 링크들을 어딘가에 만들어 놓고 각각 선택하면서 서비스하고 있는 앱이 잘 실행되는지 하려면 매우 매우 귀찮습니다. 어딘가에 리스트로 만들어 놓고 또한 링크를 타이핑 치는 게 아니라 자동으로 인식해서 처리하면 아주아주 편리할 거 같아서 한번 만들어 봤습니다. 

https://play.google.com/store/apps/details?id=com.hmju.deeplink 

 

퀵딥링크(Quick DeepLink) - Google Play 앱

딥링크 테스트를 아주 간단하게 도와주는 앱입니다.

play.google.com

 

많은 이용 바랍니다 :)

 

회고


자동 배포 시스템을 구축했을 때 도움이 될만한 글을 찾기가 너무 힘들었습니다..ㅠㅠ 죄다 기본 설정만 알려줘서.... 뭐가 맞는지 잘 모르겠다는 게 큰 어려움이었습니다.

반응형
반응형

안드로이드 앱 개발시 OkHttp 를 활용해서 HTTP 통신을 합니다. 

이때 개발중에 서버나 클라 쪽에 이슈가 생기면 API 쪽에 어떤 Query Paramter 를 날렸는지 Response 는 어떻게 오는지에 대한 핑퐁을 주고 받습니다. 일반적으로 Logcat 에서 요청한 API 를 찾아서~ 복사한다음에~ 서버팀에 알려주고..... 벌써부터 귀차니즘이 시작 됩니다..

그래서 이런 귀차니즘을 해소할 그런 것들이 없을까 하다가 옆에 iOS 개발자분이 흔들어서 요청한 HTTP 로그들을 보여주는 라이브러리인 (netfox) 를 보여주더군요..

https://github.com/kasketis/netfox

 

GitHub - kasketis/netfox: A lightweight, one line setup, iOS / OSX network debugging library! 🦊

A lightweight, one line setup, iOS / OSX network debugging library! 🦊 - GitHub - kasketis/netfox: A lightweight, one line setup, iOS / OSX network debugging library! 🦊

github.com

안드로이드도 있지 않을까 한 5분 검색하고 걍 내가 만들지뭐...해서 만들었습니다.

https://github.com/sieunju/httptracking

 

GitHub - sieunju/httptracking: 🧑🏻‍💻 앱 개발시 간단하게 HTTP 를 볼수 있습니다.

🧑🏻‍💻 앱 개발시 간단하게 HTTP 를 볼수 있습니다. Contribute to sieunju/httptracking development by creating an account on GitHub.

github.com

사용방법은 간단합니다. OkHttpClient.Builder 를 통해 Client 를 만들때 'TrackingHttpInterceptor' 를 추가 하면 됩니다.

그리고 AppliCation Class 아래와 같이 설정해주면 셋팅은 끝납니다. 

그리고 원하는 페이지에서 단말기를 흔들면 팝업이 나오면서 지금까지 요청한 API 들이 촤르륵나옵니다 :)

TrackingManager.getInstance()
            .setBuildType(isDebug)
            .setLogMaxSize(1000)
            .build(this)

자세한 사용법은 제 깃허브 안에 리드미에 설명이 나와있습니다! 사용해보시고 이런 기능 있었으면 좋겠다~ 싶은것들 이슈에 올려주시면 고민해보고 추가 해보도록 하겠습니다! 

 

사용 영상

 

 

반응형
  1. 2022.05.07 00:13

    크 ~ 디버깅 하는데 굉장히 생산성이 높아지겠군요 ~! 감사합니당

반응형
요약 (Summary)

안드로이드 프로젝트가 코틀린이 주로 구성되어 있다면 HTTP 통신 시 역/직렬화는 Kotlinx Serialization을 사용해야 합니다.

배경 (Background)

HTTP 통신시 Json Converter로는 Java 구성된 Gson이나 Moshi 가 대표적으로 있습니다. 하지만 이것들은 Java 기반이라 Kotlin에 적합하지 않은 문제가 있습니다. 예를 들면 서버에서 field 값을 안 주거나, null로 줬을 때 대응에 어려움이 있습니다. 

비교 (Compare)

Gson, Moshi, Kotlinx Converter 를 비교하기 전에 제가 Json Converter 라이브러리를 지정하는 기준은 아래와 같습니다.

- Default Argument 를 사용자가 그때그때마다 처리할 수 있는가?

- List 형식의 데이터도 Default Argument 로 처리할 수 있는가?

- Data Model에서 인자값이 NonNull 임에도 불구하고 서버에서 Null 줬을 때 Default Value를 지정할 수 있는가?

- 성능적으로 이슈가 없는가?

이러한 빡쏀(?) 조건을 충족하는 게 Kotlinx Serialization입니다. 

거두절미하고 실제 데이터 통신을 하면서 설명해보도록 하겠습니다.

각 종류별 DataModel

  • Response JsonObject-1
    {
      status : null,
      data : {
        "id" : System.currentTimeMs
      }
    }​

Response JsonObject-1&nbsp;

  • Response JsonObject-2
    {
      status : "success",
      data : {
        "id" : System.currTimeMs,
        "list" : null
      }
    }​

Response JsonObject-2

  • Response JsonObject-3
    {
      status : "success",
      data : {
        "id" : System.currTimeMs,
        "list" : ["onverter test"]
      }
    }​

Response JsonObject-3

위 캡처 화면이면 제가 요구한 조건에 충족한 역/직렬화 라이브러리는 Kotlinx 가 아주 적합해 보입니다.

How To  Use (사용법)
  • build.gradle
    plugin {
    	id 'kotlinx-serialization'
    }
    ...
    dependencies {
    	implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1'
        // Retrofit2 Converter
        implementation 'com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0'
    }
  • Data Model
    import kotlinx.serialization.Serializable
    
    @Serializable
    data class Example(
    	val id : Int = 0
    )
    // 어논테이션 Serializable 선언해주고 Default 값을 지정해주면 해당 값이 없거나 Null 인경우 자동으로 지정됩니다.
  • Json Builder
    import kotlinx.serialization.ExperimentalSerializationApi
    import kotlinx.serialization.json.Json
    
    Json {
    	isLenient = true // Json 큰따옴표 느슨하게 체크.
        ignoreUnknownKeys = true // Field 값이 없는 경우 무시
        coerceInputValues = true // "null" 이 들어간경우 default Argument 값으로 대체
    }​
  • Retrofit Json Converter
    import kotlinx.serialization.ExperimentalSerializationApi
    import kotlinx.serialization.json.Json
    import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
    
    Retrofit.Builder {
    	...
        addConverterFactory(
        Json {
    		isLenient = true // Json 큰따옴표 느슨하게 체크.
        	ignoreUnknownKeys = true // Field 값이 없는 경우 무시
        	coerceInputValues = true // "null" 이 들어간경우 default Argument 값으로 대체
    	}.asConverterFactory("application/json".toMediaType()))
    )​

비교적 사용법은 정말 간단합니다. 앞으론 서버에서 데이터 값이 Null로 인해서 죽거나 Field 값이 없어서 죽는 이슈로부터 벗어나길 바랍니다 :)

사용법 안내 

https://github.com/sieunju/kotlinxSerialization

 

GitHub - sieunju/kotlinxSerialization: 코틀린에 맞는 역/직렬화 셈플 코드 입니다.

코틀린에 맞는 역/직렬화 셈플 코드 입니다. Contribute to sieunju/kotlinxSerialization development by creating an account on GitHub.

github.com

반응형
반응형

안녕하세요.

이번에는 제가 여러 앱을 사용하면서 인상 깊었던 View 들의 애니메이션이라던지 프로젝트 개발하면서 Ui 측면에서 비용이 많이 들었던 것들을 시간 날 때마다 개발해서 사용했던 것을 소개해볼까 합니다.

https://github.com/sieunju/widget

 

GitHub - sieunju/widget: Visual 적인 View 들을 모아놓은 라이브러리입니다.

Visual 적인 View 들을 모아놓은 라이브러리입니다. Contribute to sieunju/widget development by creating an account on GitHub.

github.com

기능

- SurfaceView 기반의 ProgressView

- Coordinator.Behavior 기반의 TranslationBehavior

- CustomTextView, CustomLayout

- 점점 펼쳐지는 ParallaxView

- Gesture 를 통한 Scale or Move 처리를 할수 있는 FlexibleImageView

사용법

https://github.com/sieunju/widget/blob/master/README.md

 

GitHub - sieunju/widget: Visual 적인 View 들을 모아놓은 라이브러리입니다.

Visual 적인 View 들을 모아놓은 라이브러리입니다. Contribute to sieunju/widget development by creating an account on GitHub.

github.com

 

추후 시간이 있거나 뭔가 라이브러리를 만들고 싶을때 추가할 기능들

- Glide 에서 더 이상 BitmapTransformation 사용하지 말고 ImageView에서 Corner, Border 처리를 하자!

- 열고 닫기 기능의 Layout (RecyclerView 기반도 되면 참 좋겠다..) 

 

 

ps.) 혹시 이런 애니메이션을 가진 View 가 있었으면 좋겠다 라던지 또는 이런 CustomView 가 있었으면 좋겠다 하는 아이디어 있으시면 댓글 남겨주시길 바랍니다 :)

이상 포스팅을 마치도록 하겠습니다.!

반응형

+ Recent posts