LSA vs Streamsupport

Java8 Lightweight-Stream-API Streamsupport
java.util.function com.annimon.stream.function java8.util.function
BiConsumer<T,U> BiConsumer BiConsumer
BiConsumers
BiFunction<T,U,R> BiFunction BiFunction
BiFunctions
BinaryOperator<T> BinaryOperator BinaryOperator
BinaryOperators
BiPredicate<T,U> BiPredicate
BiPredicates
BooleanSupplier BooleanSupplier
Consumer<T> Consumer Consumer
Consumers
DoubleBinaryOperator DoubleBinaryOperator DoubleBinaryOperator
DoubleConsumer DoubleConsumer DoubleConsumer
DoubleConsumers
DoubleFunction<R> DoubleFunction DoubleFunction
DoublePredicate DoublePredicate DoublePredicate
DoublePredicates
DoubleSupplier DoubleSupplier DoubleSupplier
DoubleToIntFunction DoubleToIntFunction DoubleToIntFunction
DoubleToLongFunction DoubleToLongFunction DoubleToLongFunction
DoubleUnaryOperator DoubleUnaryOperator DoubleUnaryOperator
DoubleUnaryOperators
Function<T,R> Function Function
Functions
FunctionalInterface
IntBinaryOperator IntBinaryOperator IntBinaryOperator
IntConsumer IntConsumer IntConsumer
IntConsumers
IntFunction<R> IntFunction IntFunction
IntPredicate IntPredicate IntPredicate
IntPredicates
IntSupplier IntSupplier IntSupplier
IntToDoubleFunction IntToDoubleFunction IntToDoubleFunction
IntToLongFunction IntToLongFunction IntToLongFunction
IntUnaryOperator IntUnaryOperator IntUnaryOperator
IntUnaryOperators
LongBinaryOperator LongBinaryOperator LongBinaryOperator
LongConsumer LongConsumer LongConsumer
LongConsumers
LongFunction<R> LongFunction LongFunction
LongPredicate LongPredicate LongPredicate
LongPredicates
LongSupplier LongSupplier LongSupplier
LongToDoubleFunction LongToDoubleFunction LongToDoubleFunction
LongToIntFunction LongToIntFunction LongToIntFunction
LongUnaryOperator LongUnaryOperator LongUnaryOperator
LongUnaryOperators
ObjDoubleConsumer<T> ObjDoubleConsumer ObjDoubleConsumer
ObjIntConsumer<T> ObjIntConsumer ObjIntConsumer
ObjLongConsumer<T> ObjLongConsumer ObjLongConsumer
Predicate<T> Predicate Predicate
Predicates
Supplier<T> Supplier Supplier
ThrowableConsumer
ThrowableFunction
ThrowablePredicate
ThrowableSupplier
ToDoubleBiFunction<T,U> ToDoubleBiFunction
ToDoubleFunction<T> ToDoubleFunction ToDoubleFunction
ToIntBiFunction<T,U> ToIntBiFunction
ToIntFunction<T> ToIntFunction ToIntFunction
ToLongBiFunction<T,U> ToLongBiFunction
ToLongFunction<T> ToLongFunction ToLongFunction
UnaryOperator<T> UnaryOperator UnaryOperator
UnaryOperators
java.util.Random
java8.lang.Doubles
java8.lang.FunctionalInterface
java8.lang.Integers
java8.lang.Iterables
java8.lang.Longs
java.util com.annimon.stream java8.util
ArrayDequeSpliterator
ArrayListSpliterator
ArrayPrefixHelpers
ArraysArrayListSpliterator
ArraysParallelSortHelpers
Comparators
COWArrayListSpliterator
COWArraySetSpliterator
DelegatingSpliterator
DoubleSummaryStatistics
DualPivotQuicksort
Objects Objects Objects.java
Observable
Optional<T> Optional Optional.java
OptionalDouble OptionalDouble OptionalDouble
OptionalInt OptionalInt OptionalInt
OptionalLong OptionalLong OptionalLong
PBQueueSpliterator
PQueueSpliterator
PrimitiveIterator
RASpliterator
Spliterator
Spliterators
SplittableRandom
StringJoiner
TimSort
UnsafeAccess
VectorSpliterator
java.util.concurrent   java8.util.concurrent
CompletionException CompletionException
ConcurrentMaps
CountedCompleter<T> CountedCompleter
ForkJoinPool ForkJoinPool
ForkJoinTask<V> ForkJoinTask
ForkJoinWorkerThread ForkJoinWorkerThread
Phaser Phaser
RecursiveAction RecursiveAction
RecursiveTask<V> RecursiveTask
ThreadLocalRandom ThreadLocalRandom
TLRandom
UnsafeAccess
java.util.stream com.annimon.stream java8.util.stream
BaseStream<T,S> BaseStream
Collector<T,A,R> Collector Collector
Collectors Collectors Collectors
Compat
DistinctOps
DoublePipeline
DoubleStream DoubleStream DoubleStream
DoubleStream.Builder
DoubleStreams
Exceptional
FindOps
ForEachOps
IntPipeline
IntStream IntStream IntStream
IntStream.Builder
IntStreams
IntPair
LazyIterator
LongPipeline
LongStream LongStream LongStream
LongStream.Builder LongStreams
LsaExtIterator
LsaIterator
MatchOps
Node
Nodes
PipelineHelper
PrimitiveExtIterator
PrimitiveIterator
RandomCompat
ReduceOps
ReferencePipeline
RefStreams
Sink
SinkConsumer
SinkDefaults
SliceOps
SortedOps
SpinedBuffer SpinedBuffer
Stream.Builder<T>
Stream<T> Stream Stream
StreamOpFlag
Streams
StreamShape
StreamSpliterators
StreamSupport StreamSupport
TerminalOp
TerminalOpDefaults
TerminalSink
WhileOps
LSA vs Streamsupport

RecyclerView and Scroll

리싸이클러뷰에 있는 여러가지 스크롤 기능을 정리해 보겠습니다. 레이아웃 매니져도 스크롤에 관여하는데 그 중에서도 LinearLayoutManager에 대해서만 정리합니다. 예제 코드는 https://github.com/BattleShipPark/example.rv_scroll에 있습니다

일단 스크롤 메소드를 표로 정리하고 각각에 대해 알아 봅니다. 각 메소드마다 API 문서의 설명을 붙여 놨습니다.

RecyclerView LinearLayoutManager
scrollBy() scrollHorizontallyBy()

scrollVerticallyBy()

scrollTo()
scrollToPosition() scrollToPosition()
scrollToPositionWithOffset()
smoothScrollBy()
smoothScrollToPosition() smoothScrollToPosition()

 

scrollBy()

public void scrollBy (int x, int y)
Move the scrolled position of your view. This will cause a call to onScrollChanged(int, int, int, int) and the view will be invalidated.

스크롤 콜백이 호출된다는 것과 실제로는 LayoutManager의 scrollHorizontallyBy()와 scrollVerticallyBy()를 호출해서 구현했다는 것이 특징입니다

scrollTo()

public void scrollTo(int x, int y)
View Set the scrolled position of your view. This will cause a call to onScrollChanged(int, int, int, int) and the view will be invalidated.

스크롤 콜백이 호출된다는 것이 특징입니다.

scrollToPosition()

public void scrollToPosition(int position)
Convenience method to scroll to a certain position. RecyclerView does not implement scrolling logic, rather forwards the call to RecyclerView.LayoutManager.scrollToPosition(int)

특정 위치로 이동할 수 있는데 실제 구현은 LayoutManager.scrollToPosition()으로 위임하고 있습니다. 아래는 LinearLayoutManager.scrollToPosition()입니다.

public void scrollToPosition(int position)
Scroll the RecyclerView to make the position visible.
RecyclerView will scroll the minimum amount that is necessary to make the target position visible. If you are looking for a similar behavior to android.widget.ListView.setSelection(int) or android.widget.ListView.setSelectionFromTop(int, int), use scrollToPositionWithOffset(int, int).
Note that scroll position change will not be reflected until the next layout call.

뭔가 설명이 긴데 예제를 보겠습니다. 예제앱에서 리스트의 처음 상태는 아래와 같습니다.

1

여기서 scrollToPosition() 버튼을 누르면 6번으로 이동하게 됩니다. 그런데 6번이 오른쪽으로 붙네요?

2

기대했던건 왼쪽에 붙는건데 다시 버튼을 눌러서 12번, 18번으로 이동해 보면 동일하게 오른쪽으로 붙습니다.

34

다시 한 번 버튼을 누르면 4번으로 이동하는데, 이번에는 왼쪽으로 붙네요?

5

아.. 문서에 있는 minimum amount, target position visible이 이런 뜻이군요. 해당 아이템이 화면에 보이도록 최소한만 움직이는 겁니다. 처음에는 6번이 화면 오른쪽에 숨어 있었기 때문에 최소한으로 움직이면 6번이 화면 오른쪽에 붙는거고, 12번, 18번도 마찬가지입니다. 그 다음 4번은 화면 왼쪽에 숨어 있기 때문에 화면 왼쪽에 붙는게 최소한으로 움직이는거죠.

기존의 ListView.setSelection()에서는 화면 왼쪽으로 붙여 줬는데, 그 기능은 scrollToPositionWithOffset()을 사용하라는군요. 확인해 볼까요?

scrollToPositionWithOffset()

public void scrollToPositionWithOffset(int position, int offset)
Scroll to the specified adapter position with the given offset from resolved layout start. Resolved layout start depends on getReverseLayout(), ViewCompat.getLayoutDirection(View) and getStackFromEnd().
For example, if layout is VERTICAL and getStackFromEnd() is true, calling scrollToPositionWithOffset(10, 20) will layout such that item[10]’s bottom is 20 pixels above the RecyclerView’s bottom.
Note that scroll position change will not be reflected until the next layout call.
If you are just trying to make a position visible, use scrollToPosition(int).

설명이 엄청 기네요.. 일단 scrollToPositionWithOffset() 버튼을 눌러 봅시다. offset은 20픽셀로 되어 있습니다.

11121314

scrollToPosition()과는 다른 결과입니다. 무조건 화면 왼쪽으로 붙고 offset만큼 떨어져 있네요. 이해하기에는 어렵지 않은 결과인데, 문서에 있는 getReverseLayout()과 getStackFromEnd()는 무슨 기능일까요? 아래에서 확인해 봅니다.

getReverseLayout()

reverseLayout() 버튼을 눌러서 새 액티비티를 띄우고 체크박스를 켜고 끄면서 실행해 봅시다.

reverseLayout()을 켜면 아래 그림처럼 0번 아이템이 화면 오른쪽과 아래에서부터 시작합니다. 그리고 scrollToPosition()과 scrollToPositionWithOffset()을 사용해 보면 방향만 바뀌고 앞에서 설명한 로직대로 동작하는 것을 알 수 있습니다.

21

22

getStackFromEnd()

이번에는 stackFromEnd 버튼을 눌러서 새 액티비티를 띄워봅시다. scrollToPositionWithOffset() 문서에 보면 getStackFromEnd()에 따라 결과가 달라진다는데, 체크박스를 켜고 확인해 볼까요?

31

결과가 달라지나요? 제가 볼때는 똑같은데… 문서로 봐서는 VERTICAL이고 getStackFromEnd()==true이면 아래쪽에 붙어야 하는데 차이가 없네요. 검색해 봐도 별 내용 없는데 ㅜㅜ

그 와중에 활용할 수 있는 예를 하나 찾았는데요, 아래 그림은 add() 버튼을 누를 때 아이템을 추가하도록한 예제입니다. 그런데 아이템이 추가되도 스크롤이 맨 끝으로 유지되고 있죠? 즉 채팅처럼 아이템을 끝에 추가할 때 스크롤을 맨 끝으로 유지할 수 있는 방법입니다. 직접 스크롤을 제어하지 않더라도, setStackFromEnd(true)이면 맨 끝의 아이템을 볼 수 있습니다. 다만 여기서도 Adapter.notifyDataSetChanged()를 호출하면 안 되고 RecyclerView.setAdapter()로 어댑터를 바꿔줘야 이런 효과가 가능합니다.

32

smoothScrollBy()

public void smoothScrollBy(int dx, int dy)
Animate a scroll by the given amount of pixels along either axis.

부드러운(smooth) 스크롤을 합니다. 아래 메소드를 더 자세히 살펴보겠습니다.

smoothScrollToPosition()

public void smoothScrollToPosition(int position)
Starts a smooth scroll to an adapter position.
To support smooth scrolling, you must override RecyclerView.LayoutManager.smoothScrollToPosition(RecyclerView, RecyclerView.State, int) and create a RecyclerView.SmoothScroller.
RecyclerView.LayoutManager is responsible for creating the actual scroll action. If you want to provide a custom smooth scroll logic, override RecyclerView.LayoutManager.smoothScrollToPosition(RecyclerView, RecyclerView.State, int) in your LayoutManager.

문서를 보면 LayoutManager.smoothScrollToPosition()을 오버라이드 하라고 하는데, 핵심은 SmoothScroller를 사용하는 것입니다. 실제 LayoutManager.smoothScrollToPosition()은 아래처럼 구현되어 있습니다

LinearSmoothScroller linearSmoothScroller =
        new LinearSmoothScroller(recyclerView.getContext()) {
            @Override
            public PointF computeScrollVectorForPosition(int targetPosition) {
                return LinearLayoutManager.this
                        .computeScrollVectorForPosition(targetPosition);
            }
        };
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);

여기서 LinearSmoothScroller는 SmoothScroller의 서브클래스입니다. 그럼 오버라이드하면 유용한 메소드 몇 개를 살펴보겠습니다. 예제에서는 아래와 같이 사용했습니다.

@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
    return layoutManager.computeScrollVectorForPosition(targetPosition);
}

@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
    return super.calculateSpeedPerPixel(displayMetrics) * 3;
}

@Override
public int calculateDxToMakeVisible(View view, int snapPreference) {
    return super.calculateDxToMakeVisible(view, SNAP_TO_START) + centerOffset;
}

public PointF computeScrollVectorForPosition(int targetPosition)

이건 추상 메소드라서 무조건 구현해야 합니다만, 위에서처럼 LayoutManager의 코드를 사용하면 됩니다. 현재 위치에 따라 어느 방향으로 스크롤할 것인가를 결정하는건데 특별히 커스텀할 상황이 생각나지는 않네요.

protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics)

스크롤 속도를 조절할 수 있습니다. 픽셀당 움직이는 속도(ms)이므로 클수록  천천히 이동합니다. 기본 구현 코드가 있으므로 그걸 적당히 키워서 사용하면 됩니다.

public int calculateDxToMakeVisible(View view, int snapPreference)

이름 그대로 아이템이 완전히 보이기 위해 필요한 이동량입니다. 이건 Dx니까 횡스크롤에서 필요할거고, 종스크롤에서는 Dy메소드를 사용하면 됩니다.

이 메소드를 굳이 소개한 이유는, 이 글의 맨 위에 표를 다시 보시면 현재 LinearLayoutManager에는 smoothScrollToPositionWithOffset()이 없는데, 이 메소드를 사용하면 필요한 기능을 제공할 수 있습니다. 위에 예제에서처럼 offset을 더해주면 됩니다.

그럼 아이템이 어디를 기준으로 하고 있는가도 중요한데요, SNAP_TO_START가 그 역할을 합니다. 가능한 값은 아래와 같습니다.

SNAP_TO_START: Align child view’s left or top with parent view’s left or top

SNAP_TO_END: Align child view’s right or bottom with parent view’s right or bottom

SNAP_TO_ANY: Decides if the child should be snapped from start or end, depending on where it currently is in relation to its parent.
For instance, if the view is virtually on the left of RecyclerView, using SNAP_TO_ANY is the same as using SNAP_TO_START

스크롤 타이밍

각 스크롤 메소드에 대해 알아 봤는데요, scrollToPosition()과 scrollToPositionWithOffset() 문서를 보면 “Note that scroll position change will not be reflected until the next layout call.” 이런 설명이 있습니다. 이건 어떤 영향을 줄까요?

예제에서 “scrollToPosition(), notifyDataSetChanged()” 버튼을 누르면, 1번 인덱스로 스크롤하고 1번 위치에 20이라는 아이템을 추가하도록 했습니다. 그럼 1번 아이템이 안 보이는 상태에서 버튼을 누르면 1번이 화면에 보여야 할 것 같은데, 아래처럼 20번이 화면에 보입니다.

41

LinearLayoutManager.scrollToPosition() 코드를 보면

mPendingScrollPosition = position;
mPendingScrollPositionOffset = INVALID_OFFSET;
if (mPendingSavedState != null) {
    mPendingSavedState.invalidateAnchor();
}
requestLayout();

바로 스크롤하는게 아니라 일단 멤버에 저장하고 넘어가는걸 알 수 있는데요, 즉 예제의 경우에서는 1번 인덱스로 스크롤하도록 요청했지만, 해당 인덱스에는 20이 들어간 후에 리스트를 그리면서 스크롤 처리를 하므로 20번 기준으로 스크롤이 되는 것입니다.

자주 일어나는 상황은 아닌데, 데이터 변경과 스크롤을 동시에 해야 하는 상황에서 마음대로 스크롤이 안 되니 당황스러웠습니다 ^^;

긴 글 읽어 주셔서 감사합니다.

RecyclerView and Scroll

BATTLESHIP DEVDIARY VOL.5

RecyclerView에서 특정 아이템을 화면 가운데로 스크롤 (scroll to center with specified item in RecyclerView)

횡스크롤에서 갯수가 동적인 여러개의 그룹을 표현하는 상황이였습니다. 즉

I1 I2 I3 … 에서 I1을 즐겨 찾기를 하면

F1 Div I1 I2 I3 … 가 됩니다. 여기서 Div는 구분선인데, 아이템과 너비가 다릅니다. 게다가 특정 조건이 되면 프로모션을 위해 맨 앞에 아이템이 추가됩니다.

P1 Div I1 I2 D I3 I4 Div I5 I6 …

이 때 I5를 탭하면 I5가 화면 가운데로 스크롤하고 싶었습니다.

LayoutManager.scrollToPosition()은 아이템이 화면에 딱 걸리는 경우에 사용할 수 있으므로 지금은 적절하지 않습니다. scrollToPositionWithOffset()을 사용하면 된다고 하는건 금방 찾아냈는데… 이제 offset을 어떻게 계산한다?

화면 왼쪽이 기준이니까 선택한 아이템이 가운데에 왔을 때  화면 왼쪽에 걸리는 아이템을 찾아야 하는군. 그리고 선택한 아이템이 가운데로 오도록 offset을 계산하면 되는데.. 아이템마다 너비가 다르니 화면 왼쪽에 걸리는 아이템을 찾아 가기도 쉽지 않고, 게다가 상황마다 아이템 갯수도 다르니 고려해야 할게 점점 많아진다…. 이게 이렇게 복잡하게 할 필요가 있나 하는 생각에 포기하려는 순간….!

복잡하게 계산할 것 없이 그냥 선택한 아이템 자체만 고려해서 계산하면 되는거네요… 아래처럼 한 줄로.. 하아…

LayoutManager.scrollToPositionWithOffset(position, (screenWidth + itemWidth) / 2);

안드스튜디오 디자이너에서 뷰 배경에 그라데이션 넣기 (Gradation in view background in Android Studio Designer)

리소스에 <shape><gradient>를 넣으면 그라데이션 효과를 쉽게 넣을 수 있습니다. 디자인 가이드에 맞게 angle을 어떻게 넣어야 하는지 궁금해서 이러저리 값을 바꾸면서 안드로이드 스튜디오 디자이너에서 확인해 보는데, 제대로 적용이 안 되는겁니다.. 그래서 startColor와 endColor에 알파를 잘못 넣었나 싶어서 바꿔보고, centerX, centerY를 넣어야 하는건지 싶어서 바꿔보고, 한참 해봐도 뭐가 제대로 된 설정인지 알 수가 없었습니다.

그러다 혹시나 해서 빌드를 해서 단말에서 실행해 보니 색깔이 잘 나옵니다… 안드로이드 스튜디오의 디자이너 오류인가 보네요.. 슬프다..

BATTLESHIP DEVDIARY VOL.5

BattleShip DevDiary Vol.4

Retrofit2, etag, 캐시

REST API로 데이터를 가져와서 화면에 보여줄 때 네트웍 결과만 기다릴 수는 없으니 캐시를 사용하게 됩니다. 일단 캐시의 내용으로 화면에 보여주고, API 결과로 다시 보여주는 방식이죠. 그런데 retrofit2를 처음 사용해 보면서 이 부분에서 삽질을 좀 했습니다.

일단 캐시를 따로 만들지 않고 OkHttp 자체 캐시를 사용해 보려고 했습니다. 요청 헤더에 “Cache-Control”을 사용하면 서버에는 요청하지 않고 캐시만 읽을 수 있습니다만 실행해 보니 캐시를 못 읽네요. 원인은 정확히 모르겠지만, 우리쪽 서버 설정에서 etag를 사용하기 위해  max-age=1을 내려주기 때문에 캐시가 유효하지 않다고 판단한 것 같습니다.

그럼 캐시를 따로 만들려면, retrofit에서 URL을 뽑아 와야 하는데 이 부분에서도 좀 헤맸습니다. 실제로는 이런 식으로 얻을 수 있는데요,

T service = retrofit.create(serviceClazz);
Call<R> call = action.call(service);
String url = call.request().url().toString();

사실 방법은 간단한데 여기서 걱정했던건, action.call()을 하면서 서버 요청이 발생할 것 같다는 생각이 들어서 이 코드를 사용하지 못하고 이리저리 헤맸습니다. 사실 retrofit 사용법을 잘 보면 call.execute()를 해주도록 하고 있거든요, 서버 요청은 여기서 이루어지는 거죠. 그러므로 위의 코드에서처럼 url을 얻어 오고, 그걸로 캐시를 읽으면 됩니다.

그러면 서버 요청할 때 etag는 어떻게 넣느냐 하면, 아래와 같은 방법으로 합니다.

OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
httpClient.addInterceptor(chain -> {
    Request original = chain.request();
    Request.Builder builder = original.newBuilder()
            .header("User-Agent", HandyHttpClientImpl.userAgent)
            .method(original.method(), original.body());

    if (etag != null) {
        builder.header("If-None-Match", etag);
    }
    return chain.proceed(builder.build());
});

Retrofit retrofit = new Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())
        .baseUrl(ServerTypeHelper.getApiServer())
        .client(httpClient.build())
        .build();

User-Agent를 넣는다거나 Gson 컨버터를 넣는다거나 하는 부분이 좀 더 들어가 있지만 중요한건 builder.header()입니다. 간단하게 처리해 주는 방법은 없고 직접 헤더를 만들면 된다..라는 단순한 결론이네요.

Retrofit으로 캐시를 직접 읽는 작업을 해보니, 이런 생각이 들더군요. 이거 retrofit을 안 쓰고 직접 Http Client를 만지는 것보다 좋은게 뭐지? 그리고 얻은 결론은, 이 라이브러리의 개발 의도는 Http Client를 간편하게 사용해 보자인데, 캐시를 직접 읽는 식의 복잡한 작업을 해버리는건 개발 의도와 맞지 않다. 오히려 더 거추장스러울 수도 있다.

생산성 향상을 위해 무조건 라이브러리를 도입하기 보다는 상황에 맞는 라이브러리를 도입해야겠다는 교훈을 얻었습니다.

Mockito ExceptionInInitializerError, multidex

mockito를 사용하는데 갑자기 예외가 발생합니다.. 다행히도 해결방법이 있네요 http://stackoverflow.com/questions/29290795/attempt-to-mockito-mock-any-class-generates-exceptionininitializererror 멀티덱스에서 추가로 필요한게 있었군요

어라 근데 이렇게 하면 dexmaker에서 예외가 발생합니다…

java.lang.NullPointerException: Attempt to invoke virtual method
'java.lang.Class java.lang.Object.getClass()' on a null object reference
at com.google.dexmaker.mockito.DexmakerMockMaker.getInvocationHandlerAdapter(DexmakerMockMaker.java:80)
at com.google.dexmaker.mockito.DexmakerMockMaker.getHandler(DexmakerMockMaker.java:75)
at org.mockito.internal.util.MockUtil.isMockitoMock(MockUtil.java:74)

으.. 다시 찾아 헤매입니다. 결국 어떤 상황이냐면 https://code.google.com/archive/p/dexmaker/issues/2 원래는 이쪽에서 관리되던건데 github로 프로젝트를 옮기면서 https://github.com/crittercism/dexmaker/pull/24에서 버그는 수정되었지만 릴리스되지는 않았습니다. 직접 코드를 다운로드 받아서 사용하는 방법밖에 없네요…

SDK Manager plugin, Error:Cause: com.android.sdklib.repository.FullRevision

다른 프로젝트의 gradle 파일을 보다 보니 생소한 플러그인이 있네요. 그러나 이것은 그 유명한 Jake Whaton옹이 만든 그레이들 플러그인입니다. 개발하면서 여러 프로젝트를 돌리다 보면 항상 빌드에 필요한 SDK나 서포트 라이브러리의 특정 버전을 다운로드 받아야 하는 번거로움이 있는데, 이 플러그인을 사용하면 빌드에 필요한 SDK 등을 다운로드해 줍니다.

그런데 막상 빌드를 해보면 에러가 발생합니다.. 여기에 대한 버그 수정은 됐는데https://github.com/JakeWharton/sdk-manager-plugin/pull/100, 이것도 역시 릴리스는 안 되어 있습니다.. 직접 코드를 받아서 빌드해야 하는 고민을 해보지만, 훌륭하신 분들이 이미 해결해 놨습니다.

repositories {
    maven { url 'https://jitpack.io' }
}

classpath 'com.github.JakeWharton:sdk-manager-plugin:220bf7a88a7072df3ed16dc8466fb144f2817070'

이렇게 하면 특정 커밋을 기준으로 라이브러리를 사용할 수 있네요. 바로 위의 dexmaker로 여기를 통하면 소스를 직접 가져오지 않아도 최신 버전을 사용할 수 있습니다. 만세~

APK의 원래 크기는 왜 설치된 후에 더 커질까

크게 신경 쓰지 않았던 부분인데, 빌드의 결과로 나온 apk를 단말에 설치해 보면 더 많은 공간을 차지하고 있습니다. 이유가 궁금해서 좀 찾아 보니 http://stackoverflow.com/questions/14409139/why-does-my-app-size-on-device-differ-than-the-apk-or-play-store-size. 앱을 설치하면 apk 파일은 그대로 두고 dex 파일을 다시 설치하는군요. 그래서 단말에서는 원래 apk 파일 크기의 두 배 정도를 차지하는걸로 나오네요

BattleShip DevDiary Vol.4

BattleShip DevDiary Vol.3

errorprone

구글에서 만든 자바용 정적 코드 분석 도구입니다(http://errorprone.info). 실행해보면 의외로 도움이 되는 부분도 있고, 쓸데 없는 내용도 많이 나옵니다 ^^; 저는 코드 분석 도구를 쓰고 있지 않지만, lint 등을 항상 사용하시는 분들은 도움이 될 것 같습니다.

안드로이드 스튜디오 2.0

1.5에서 2.0으로 올리면서 두 가지 문제가 있었는데요

1. 릴리스 모드에서 lint가 동작한다

로컬에서는 문제 없었는데 CI 서버에서 릴리스 빌드를 하니까 lint가 동작하면서 lint 에러 때문에 진행이 안 되네요. 옵션을 설정해 줄 수 있는데 아예 lint를 동작하지 않게 하는 방법은 이렇습니다

lintOptions {
checkReleaseBuilds false
}

2. Test Artifacts

테스트 모드를 전환하기 위해서는 Build Variants에서 Test Artifacts를 변경했는데, 이제는 그럴 필요 없습니다. 모든 테스트가 한 번에 빌드됩니다. 설정 – Build, Execution, Deployment – Build Tools – Gradle – Experimental에 Enable all test Artifacts… 를 체크하면 번거롭게 모드를 전환할 필요가 없습니다

안드로이드 스튜디오에서 Gradle 업그레이드

Project Structure 메뉴에서 Project를 보면 gradle 버전을 적는 부분이 있습니다.

com.google.gson.internal.LinkedTreeMap cannot be cast to

gson을 사용해서 파싱을 하는데 처음 보는 예외가 발생하네요.TypedValue로 파싱할 클래스를 명시했는데도 문제가 생깁니다. 원인은 반환값에 타입 파라미터를 사용했기 때문입니다. 보통은 이런 식으로 파싱을 할텐데

Type resultContainer = new TypeToken<ResultContainer<Detail>>() {
}.getType();

ResultContainer<Detail> result = gson.fromJson(json, resultContainer);

이걸 좀 더 일반화하기 위해 아래와 같이 타입 파라미터를 이용해서 수정했습니다.

Type resultContainer = new TypeToken<ResultContainer<R>>() {
}.getType();

ResultContainer<R> result = gson.fromJson(json, resultContainer);

그런데 제네릭은 컴파일 시간에만 의미가 있고, 실행 시간에는 지워지는 정보라 파싱을 하는 시점에는 ResultContainer 안에 무슨 타입이 들어가는지 gson은 알 수가 없습니다. 그래서 자체적으로 map을 만들어서 반환하면서 생기는 문제입니다.

반환값에 타입 파라미터를 사용하고 싶을 때는 아래와 같이 명시해서 사용해야 합니다. clazz는 T 타입의 클래스 변수입니다.

T response = gson.fromJson(json, clazz);

이 때 T는 ResultContainer<R>처럼 제네릭을 사용하면 안 됩니다. 당연하지만 그러면 클래스 변수를 가져올 수가 없기 때문입니다.

API23에서 Robolectric

compileSdkVersion을 23으로 올리니까 Robolectric이 동작하지 않습니다.. 3.1-SNAPSHOT을 사용하면 된다는데, 이후에도 메이븐으로 뭔가 설정을 해줘야 하네요. 일단 후퇴..

LeakCanary

https://github.com/square/leakcanary 안드로이드에서 메모리 누수를 잡아 주는 라이브러리입니다. 레퍼런스 때문에 activity가 메모리에서 해제되지 않는 문제는 MAT 돌리는 것보다 훨씬 편하게 위치를 찾을 수 있네요.

WebView에서 localStorage 사용하면서 NPE

웹뷰를 사용하는데 화면이 안 보여서 로그를 보니 자바스크립트쪽에서 NPE가 발생합니다. 위치를 보니까 local storage를 사용하는 곳입니다. 안드로이드에서는 WebSettings.setDomStorageEnabled(true)를 실행해 줘야 저장소를 사용할 수 있습니다.

Mac에서 JAVA 위치 조정

Retrolamda를 사용하려고 java8로 빌드를 하는데 맥에서 java8 위치를 못 찾습니다. 환경 변수 설정으로는 해결이 안 되고, gradle 설정으로 복잡하게 해결하는 방법도 있던데, 그냥 Project Structure 메뉴에 JDK 위치를 지정해 주는 부분이 있어서 그걸 수정했습니다.

Lombok 빌드 속도

프로젝트에 몇 가지 라이브러리를 추가했더니 빌드 속도가 엄청 느려졌습니다. 찾아 보니 lombok이 문제네요… lombok 없이 38초 걸리던 빌드가 lombok를 추가하면 48초가 됩니다…

Parceler에서 상속 구조의 객체를 필드로 저장할 때

안드로이드 Parcel 코드를 자동으로 생성해 주는 parceler를 도입해 봤는데, 군더더기 코드가 싹 없어 집니다. 물론 문제가 발생하는 부분도 있네요

class Outer {
    Parent s = new Child();
}

이런 Outer 클래스를 parceler로 처리해서 parcel에 넣었다가 빼면, s가 Child가 아닌 Parent로 인식됩니다. parceler에서 생성해 주는 코드가 상속 구조를 생각하지 않기 때문인데요. 이럴 때 ParcelPropertyConverter를 이용해서 다른 방법으로 read/write하도록 알려줘야 합니다.

class Outer {
    @ParcelPropertyConverter(Converter.class)
    Parent s = new Child();

    public static class Converter implements ParcelConverter<Parent> {
        @Override
        public void toParcel(Parent input, Parcel parcel) {
            parcel.writeParcelable(Parcels.wrap(input), 0);
        }

        @Override
        public Parent fromParcel(Parcel parcel) {
            return Parcels.unwrap(parcel.readParcelable(Parent.class.getClassLoader()));
        }
    }
}
BattleShip DevDiary Vol.3

BattleShip DevDiary vol.2

BottomSheet in Android Support Library 23.2

구글 맵 등에서 사용하는 BottomSheet라는 레이아웃이 있는데, 그 동안 직접 구현해서 사용해 왔습니다. 그런데 이번에 공개된 ASL 23.2에 해당 기능이 포함되어 있네요. 사용하기에도 간편한 것 같습니다. http://kunny.github.io/lecture/ui/2016/02/28/support_bottomsheet_behavior_basics/

Assertj를 안드로이드에서 사용하려면 1.x를

최신 버전의 Assertj를 빌드에 포함시키니까 오류가 발생해서 찾아 보니, 안드로이드에서는 1.x 를 사용해야 합니다. 내부적으로 뭘 사용하고 있길래… http://joel-costigliola.github.io/assertj/assertj-core.html

밑이 2인 Log를 계산하려면?

Math 패키지에는 밑이 10과 e에 대한 로그밖에 없습니다. 밑이 2인 로그는 간단한 수학으로 계산해야 하는군요.

log2(x) = log(x)/log(2)

사용하지 않는 리소스 제거

이전에는 AndroidUnusedResources.jar를 사용했는데, 이게 gradle에서는 제대로 동작 안 하네요. https://github.com/lsit81/android-unused-resources 에서 해결했다고 하지만(한국분인듯 ^^) 저는 실패…

간단하게 안드로이드 스튜디오에 있는 Inspect 기능을 사용하면 됩니다. 검사 내용 중에 unused resource가 포함되어 있습니다.

특정 코드의 문자가 Canvas.drawText()로 표시되지 않는 문제

다음과 같은 유니코드의 문자는 글자 크기를 256이 넘게 표시할 때, 즉 257부터는 Canvas.drawText()로 비트맵에 그릴 때 표시되지 않는 오류가 있습니다: \u2194 ~ \u2199

단순히 텍스트 뷰로 문자를 보여줄 때는 글자 크기에 상관없이 표시가 되고, drawText()로 256이하의 크기로 비트맵에 그릴 때는 잘 표시됩니다. 또한 해당 코드는 텍스트뷰든 비트맵에 그리든 색깔을 바꿀 수가 없습니다.

Glide에서 이미지 크기 조정

glide가 사용하기는 쉬웠는데 고급 기능을 사용하려고 하니까 쉽지 않네요. 옵션에 따라 이미지 크기가 어떻게 조절되는지 간단히 확인해 봤습니다. 조건은, 2560×1600 이미지를 1440×2560의 해상도에서 into()로 직접 이미지뷰에 뿌리는 방식입니다. 이미지 크기는 listener()의 onResourceReady()에서 출력했습니다.

  • 기본 : 1440×900
  • fitCenter() : 1440×900
  • override(720,720) : 720×450
  • override(2880,2880) : 2880×1800
  • override(720,720), fitCenter : 720×450
  • override(2880,2880), fitCenter : 2880×1800
  • getSampleSize() : 리스너에서는 크기가 바뀌지 않는데, 이미지 품질은 바뀐다
  • override, sampleSize : 곱해서 같은 크기라면 sampleSize에서 처리하는게 조금 빠르다.
  • 큰 이미지를 sampleSize=1로 놓고 읽으니 OOM 발생. 일단 sampleSize로 디코딩을 하고 override로 크기를 다시 맞추는듯

glide github에서 이슈를 조금 보다 보니, glide의 기본 원칙은 무난한 상황에서 최고의 효율을 내기 위함인 것 같습니다. 그래서 into()로 뷰를 지정할 경우 해당 뷰의 크기에 맞춰서 이미지를 스케일링하도록 한다네요. 화면의 1픽셀이 이미지의 1픽셀에 대응하도록 해서 이미지 품질도 유지하고 메모리 효율도 높이는 거죠. 그런데 그 과정에서 override()로 임의의 크기로 만들 수 있고, 그 전에 sampleSize()로 디코딩을 먼저 하는 것 같습니다.

동작이 더 궁금하기는 한데, 이렇게 옵션을 바꿔보면서 확인해 보기보다 차라리 소스를 분석해 보는게 더 도움이 될 것 같아서 시간 되는대로 도전해 보려고 합니다.

Jack으로 빌드하기

Jack이라는게 있다고는 들었는데 관심을 못 가지다가 이 소식을 접하게 되었습니다 : https://developer.android.com/intl/ko/preview/j8-jack.html. 자바8의 람다를 안드로이드에서 사용할 수 있게 되는데 그러려면 안드로이드 스튜디오 2.1과 Jack이 필요합니다.

2.0도 정식 버전이 안 나온 상태에서 2.1을 기대하는건 너무 이른 것 같고, Jack을 한 번 사용해 보고 싶어서 현재 작업 중인 프로젝트를 Jack으로 빌드해봤습니다. 컴파일 중에 에러가 나서 이상하다 했지만 apk를 만들길래 잘 되나 싶었는데, 단말에 설치하는 과정에서 INSTALL_FAIL_DEX_OPT 가 발생합니다. 로그캣에서도 오류 메시지가 나오는데 제조사별로 차이가 있습니다

  • LG : com.android.server.pm.PackageManagerException: scanPackageLI
  • 삼성 : DexFile_isDexOptNeeded failed to open oat file

너무 막막한 상황이였는데 다행히(?) 다음과 같은 오류 보고가 되어 있네요: https://code.google.com/p/android/issues/detail?id=82486. 윈도우즈에서 빌드가 안 된다는 내용인데요, 첫 보고 날짜가 좀 지나고 동일 증상에 대한 보고도 여러개인데 구글쪽 코멘트가 없네요.. Jack은 다시 깊게 묻어 둬야 할듯…

BattleShip DevDiary vol.2

BattleShip DevDiary vol.1

Korean articles is here: https://battleshippark.wordpress.com/2016/02/29/battleship-devdiary-vol-1/

Handling click event consecutively coming from view in Android

When developing UI, there are situations when click events are coming consecutively from view like button and we have to prevent it. For example, if I press camera shutter button and close button crash may occur. (I’m not sure I have to prevent, but) There are solutions like below.

1. android:splitMotionEvents=”false”

http://developer.android.com/reference/android/R.attr.html#splitMotionEvents

If I put splitMotionEvents=”false” as property in layout, it prevents click events occur simultaneously between child views. Precautions are:

  1. You have to apply to direct parent of views for handling. It does not apply to all of children even though you apply to the most upper layout.
  2. You have to specify it whenever you need although default value is false in document.

2. Ignoring events in a certain time period

It is so much complicated to handle all of views in screen like this. We can ignore other events in a certain period after a click event occurs. There are ways like implementing Handler or comparing timestamp all the time, but I tried to implement with RxJava as a study. I thought I need proper operator and I found out this:

throttleFirst(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())

http://reactivex.io/documentation/operators/sample.html

Next-generation Graphics API – VULKAN

Graphics API following OpenGL was announced. It’s worth to have interest even although we can’t use in Android. https://developer.android.com/ndk/guides/graphics/index.html

Using MAT in Android Studio

It was very convenient to use MAT in Eclipse because it has MAT plugin. Although Android Studio(current 1.5.1) has heap analysis, it is inconvenient yet. Of course, you can download standalone application: http://www.eclipse.org/mat/downloads.php, but you can’t read heap dump with it after you get it from Android Studio. It seems version unmatch between them, and there is hprof-conv.exe in SDK-directory/platform-tools/. After you convert heap dump with this in command list, you can use MAT.

Checking Activity Stack

I was curious how activities are accumulated, and I found out like this:

adb shell dumpsys activity activities | grep -i run

There are more good functionalities in “adb shell dumpsys”.

Running external tools in Android Studio

Although this is helpful when you develop with NDK, you can use it in different situations, which is how to run external tools like javah. http://blog.burt.pe.kr/javah%EB%A5%BC-androidstudio%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0/

Building FFMPEG in Android

I introduce a well-organized document:  https://enoent.fr/blog/2014/06/20/compile-ffmpeg-for-android/

Using java.util.stream package of Java 8 in Android

When I use retrolambda, I can use lamda but can not use stream package. By the way, there is backport library here: https://sourceforge.net/projects/streamsupport/

YUV format and converting to NV21

I had to convert bitmap to NV21. I referred to here about format: http://blog.dasomoli.org/265, and here about converting:  http://stackoverflow.com/questions/5960247/convert-bitmap-array-to-yuv-ycbcr-nv21.

However, there is a bug that makes crash when image whose width or height is odd is given. I will share fixed code separately.

Rounding in Canvas.drawPath()

You can use Paint.setPathEffect(): http://stackoverflow.com/questions/7608362/how-to-draw-smooth-rounded-path

 

BattleShip DevDiary vol.1