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")
}