What’s new in Android Studio 3.1

도입

안드로이드 생태계에 큰 변화를 주었던 Android Studio 3.0 이 업데이트 되고 난 후 약 4개월 가량 지난 지금, Android Studio 3.1 이 정식 배포되었습니다.

따라서 본 글에서는 Android Studio 3.1 에 어떠한 변경점이 생겼는지 알아보려 합니다. 본 글은 Android Developers blog 의 Android Studio 3.1 글에서 요약하고 번역했습니다.

개발 분야

코틀린 Lint Check

2017년 5월 코틀린이 안드로이드의 공식 언어로 지정된 이후로 안드로이드 스튜디오에 대한 코틀린 지원을 계속해왔는데, 이제 Lint (코드 검사) 기능을 Commend Line 단에서 실행시킬 수 있게 되었다.

아주 간단히, ./gradlew lint를 입력하면 된다.

데이터베이스 수정

inline된 SQL, Room 쿼리 문을 작성할 때 좀 더 쉽게 작성할 수 있게 도와준다.

인텔리제이 업데이트

Intellij IDEA 2017.3. 에 변경된 부분을 포함한다. 주로 자동완성시 캐스팅 기능 강화, Stream 체이닝에서의 null-check, NonNull/Nullable 부착에 대한 Quick Fix, StringBuilder -> Stream collect 변환, sort 기능에 대한 Stream 변환, Deprecated 된 메서드에서 권장되는 코드로의 빠른 변환, JetBrains 계정을 통한 IDEA 설정 동기화 등이 추가되었다. 더 자세한 사항은 What’s new in Intellij IDEA 를 참고하자.

빌드 분야

새로운 컴파일러

Dex 를 컴파일하는 DX Compiler 가 대체되고, 새로운 D8 Dex Compiler 가 도입되었다. 소개하기로는 10~15%의 컴파일 시간이 개선되었다고 한다. 자세한 사항은 Next-generation Dex Compiler Now in Preview 를 참고하자.

빌드 결과 창 개선

이제 트리-뷰로 어느 부분에서 오류가 발생했는지 빠르게 보여주게 되었다.

테스트 분야

에뮬레이터 스냅샷

에뮬레이터 개선을 통해 6초 이내에 에뮬레이터 세션을 다시 시작할 수 있게 해주는 기능이다. 모든 에뮬레이터에 적용되며, 설정을 통해서 더 자세히 설정할 수 있다.

최신 버전 시스템 이미지 전달

24(누가) – 27(오레오) 버전의 에뮬레이터에는 구글 플레이 스토어와 구글 API가 내장된다. 추가적으로 윈도우 프레임이 없는 가상 기기를 만들 수 있으며, 이를 통해 18:9 화면 비율이나 DisplayCutout 를 테스트할 수 있게 된다.

최적화/디버깅 분야

C++ CPU 프로파일

Android Studio 3.0 에서 발표된 Android profiler 기능이 좀 더 강화되어, C++ 코드를 프로파일링 할 수 있게 되었다.

네트워크 스레드 & Request 프로파일

네트워크 스레드나 Request에 대해서도 프로파일링 할 수 있게 되었다.

마무리

마이너 버전만큼 변경되는 사항 자체는 적지만, 개인적으로 느끼기에는 Intellij IDEA 2017.3 에 적용된 Stream 변환 기능과 D8 컴파일러가 제일 마음에 든 것 같다.

3.1이 안정화 버전에 올랐다고 해도, 이미 3.2에 대한 베타가 진행되고 있기 때문에 그쪽도 사용해보는 것이 좋을 것 같다.

Kotlin – filterIsInstance with reflection (NumberPicker text color)

최근 개발하면서 NumberPicker 를 사용하는 일이 잦아졌는데, NumberPicker의 텍스트 색상까지 변경할 수 있어야 했었다.

대충 인터넷에 찾아본 바로는 선택된 텍스트 색상은 NumberPicker의 child 중에서 EditText 인 것을 찾아 텍스트 색상을 설정하면 되고, 선택되지 않은 부분(바퀴 부분) 은 mSelectorWheelPaint 라는 private field 에 접근하여 수정한다는 것 같다.

1. 기존에는?

보통 자바로 짜면, 아래와 비슷한 코드가 나올 것이다.

/**
 * TestPicker.java
 * Created by Pyxis on 2017. 9. 2..
 */

public class TestPicker extends NumberPicker{
    public TestPicker(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public TestPicker(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void setTextColor(int color) {
        int count = getChildCount();

        for (int i = 0; i < count - 1; i++) {
            View view = getChildAt(i);
            if (view instanceof EditText) {
                try {
                    EditText editText = (EditText) view;
                    editText.setTextColor(color);

                    Field selectorWheelPaint = NumberPicker.class.getDeclaredField("mSelectorWheelPaint");
                    selectorWheelPaint.setAccessible(true);
                    ((Paint)selectorWheelPaint.get(this)).setColor(color);
                    invalidate();
                } catch (Exception e) {
                    Log.e(TestPicker.class.getSimpleName(), "Reflection failed!");
                }
            }
        }
    }
}

NumberPicker 클래스를 상속받는 클래스를 만들어, setTextColor 란 메소드를 추가적으로 만든다.

setTextColor 안에는 자식의 갯수만큼 반복을 돌려, 해당 View의 실제 타입이 EditText인지 체크해서 텍스트 색상을 설정하거나, mSelectorWheelPaint에 접근해 색상을 설정하는 것이다.

2 .코틀린에서

코틀린에서 instanceOf의 역할을 하는 것은 is인데, 이런 식으로 사용한다.if (view is EditText)

위에 있는 자바를 그대로 변환해보자.

class TestPicker constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : NumberPicker(context, attrs, defStyleAttr) {

    fun setTextColor(color: Int) {
        val count = childCount

        for (i in 0 until count) {
            val view = getChildAt(i)
            if (view is EditText) {
                tryCatch {
                    view.setTextColor(color)

                    val selectorWheelPaint = NumberPicker::class.java.getDeclaredField("mSelectorWheelPaint")
                    selectorWheelPaint.isAccessible = true
                    (selectorWheelPaint.get(this) as Paint).color = color
                    invalidate()
                }
            }
        }
    }
}

지난 글에서 코드를 조금이라도 줄여줄 수 있게 만든 tryCatch 메소드를 사용하고,  until 이라는 infix 메소드를 사용해서 간편하게 표현했다.

3. filterIsInstance 소개

코틀린에도 자바8에서 소개된 Stream API가 있는데, .map,  .filter, .flatMap  등을 전부 제공한다.

이 기능을 사용하여 일일히 for-loop를 돌리지 않아도 처리가 가능하다.

3-1. 그전에 Stream API 가 뭐지?

Stream API는 자바8부터 소개된 기능으로 컬렉션의 요소를 하나씩 참조해서 처리할 수 있게 하는 객체다.

이해하려면 이 글을 보는 것이 아니라 다른 글을 보는 것이 좋을 정도기는 한데, 일단 예제 코드를 짜보자.

A, a, B, b 4개의 문자열이 담긴 ArrayList를 모두 대문자로 바꿔 출력하는 코드이다.

public static void main(String... args) {
        ArrayList<String> list = new ArrayList();
        list.add("A");
        list.add("a");
        list.add("B");
        list.add("b");

        System.out.println("original list");
        for (String item : list) {
            System.out.println(item);
        }
        System.out.println();

        System.out.println("~JDK7, Modify UPPERCASE");
        for (String item : list) {
            System.out.println(item.toUpperCase());
        }

        System.out.println();

        System.out.println("Using Stream API, Modify UPPERCASE");
        list.stream()
                .map(String::toUpperCase)
                .forEach(System.out::println);
    }

출력 결과는 아래와 같다.

original list
A
a
B
b

~JDK7, Modify UPPERCASE
A
A
B
B

Using Stream API, Modify UPPERCASE
A
A
B
B

3-2 조금 더 복잡한 예제

지금은 간단한 예제지만, 만일 아래와 같은 로직을 구현한다고 해보자.

  • 유저 리스트를 데이터베이스로부터 가져온다.
  • 유저 타입이 BANNED 인 유저를 필터링한다.
  • 필터링한 유저 리스트를 오름차순 정렬한다.
  • 한명 한명씩 출력한다.

유저 클래스는 아래와 같다.

 public class User {
        public static final int USER_OK = 0;
        public static final int USER_BANNED = 1;
        
        private int status = USER_OK;
        private long id = 0;
        private String name = "";
        private String email = "";
        public String getName() {
            return name;
        }

        @Override
        public String toString() {
            return "USER{id = " + id + " , name = " + name + " , email = " + email + ")";
        }
}

그러면, 기존 방식대로 구현해보자.

List<User> userList = db.getAllUserList();

ArrayList<User> bannedList = new ArrayList<>();
for (User user : userList) {
    if (user.status == User.USER_BANNED) {
        bannedList.add(user);
    }
}

Collections.sort(bannedList, new Comparator<User>() {
    @Override
    public int compare(User o1, User o2) {
        return o1.getName().compareTo(o2.getName());
    }
});

for (User user : bannedList) {
    System.out.println(user.toString());
}

이번엔 스트림으로 구현해보자.

List<User> userList = db.getAllUserList();

userList.stream()
        .filter(user -> user.status == User.USER_BANNED)
        .sorted(Comparator.comparing(User::getName))
        .forEach(System.out::println);

조금 그림과 같이 설명하자면…

Stream API diagram with example code

아이디어 출처: Processing Data with Java SE 8 Streams, Part 1 by Raoul-Gabriel Urm (http://www.oracle.com/technetwork/articles/java/ma14-java-se-8-streams-2177646.html)

  • 맨 위에서는 데이터베이스에서 가져온 유저 전체 리스트가 있다.
  • 그 다음, filter 로 type가 USER_BANNED 인 유저만 가져온다.
  • 그다음, Comparator.comparing 로 유저의 이름을 기준으로 정렬한다.
  • 마지막으로 한 개씩 꺼내서 순서대로 출력한다.

이전까지 구현했던 코드에 비해 많이 간결해졌음을 알 수 있다.

이처럼 스트림은 처리 코드를 람다로 제공하고, 중간 과정이 별도로 있어 변수를 추가로 선언하지 않아도 사용이 가능하다는 장점이 있다.

그 외에도 병렬 처리 프로그래밍에서 동시성에 대한 보장을 해주기도 한다.


 

… 이제 본론으로 돌아와서, 코틀린의 스트림(?)도 자바 스트림과 비슷하다.

다만 List의 확장 메소드로 제공하기에 사용하기 전에 Stream 객체를 얻을 필요와 없다는 점과 다양한 필터를 제공한다는 점에서 약간 다르다.

filterIsInstance 는 말 그대로 Instance, 즉 주어진 객체의 실제 타입을 체크해서 맞는 객체만 리턴한다.

자바라면, .filter(user -> user instanceof User) 정도가 된다.

실제 사용할 때에는 .filterIsInstance<Int>() 식으로 체크하고 싶은 타입을 선언하면 된다.

4. 실제로 사용해보자.

class TestPicker constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : NumberPicker(context, attrs, defStyleAttr) {
    fun setTextColor(color: Int) {
        val count = childCount

        (0 until count)
                .map { getChildAt(it) }
                .filterIsInstance<EditText>()
                .forEach {
                    tryCatch {
                        it.setTextColor(color)
                        val selectorWheelPaint = NumberPicker::class.java.getDeclaredField("mSelectorWheelPaint")
                        selectorWheelPaint.isAccessible = true
                        (selectorWheelPaint.get(this) as Paint).color = color
                        invalidate()
                    }
                }
    }
}

(0 until count) 라는 조금 특별한 문구가 보이는데, 0 에서 부터 count – 1 까지의 IntRange 를 리턴한다.

IntRange는 Itreable 를 구현하고 있어 Iterable.map의 사용이 가능하다.

원본 코드는 아래와 같다.

/**
 * Returns a range from this value up to but excluding the specified [to] value.
 * [this] 로 주어진 값부터 [to] 로 주어진 범위를 리턴하는데, value 자체는 제외한다.
 * 
 * If the [to] value is less than or equal to [Int.MIN_VALUE] the returned range is empty.
 * 만일 [to] 가 Int.MIN_VALUE 보다 적거나 같을 경우, 리턴되는 Range는 비어있습니다.
 */
public infix fun Int.until(to: Int): IntRange {
    if (to <= Int.MIN_VALUE) return IntRange.EMPTY
    return this .. (to - 1).toInt()
}

그다음 map로 자식들의 리스트를 넣고, filterIsInstance로 타입이 EditText 인 것을 찾아 forEach로 하나씩 수행한다.

그 뒤는 변환한 코틀린 코드와 다를 바가 없어진다.

마무리

내가 코틀린을 적극적으로 쓰게 된 이유중 하나가 당시 안드로이드는 Java 8 language feature 지원이 매우 빈약했기에 나름대로 대안을 찾은 것이다.

그나저나, 정작 글의 키워드였던 filterIsInstance 보다도 스트림 소개 글이 더 길어진 것 같은 기분이다(..)

** 추가

도식도의 사진이 이미지 최적화로 인해 깨진 것 같아서, 원본 링크를 남겨두니 필요한 사람이 있으면 이 쪽을 참고하면 괜찮을 것이다.