Read byte array from binary data of retrofit response

현재 서버에서 이미지를 binary data로 내려주면 클라이언트에서는 데이터를 읽은 후 byte array로 변환하고 있다. 이전에는 DEPRECATEDvolley 라이브러리를 이용해 Request를 커스텀하게 수정해서 사용했다.

public class ByteArrayRequest extends Request<byte[]> {
  private Response.Listener<byte[]> listener;
  …
  @Override
  protected Response<byte[]> parseNetworkResponse(NetworkResponse response) {
     return Response.success(response.data, HttpHeaderParser.parseCacheHeaders(response));
}

그리고 요즘에는 volley에서 Retrofit + RxJava 라이브러리로 변경하는 작업을 진행하는 중이다.

@GET(“books”)
Observable<Result<ResponseBody>> getImage(@Query(“id”) String id);

Retrofit으로 byte[]로 데이터 가져오기

Retrofit은 프로덕션에 적용한 지 얼마 안되었지만 Result에서 result.response()를 통해 얻은 객체 Response에는 크게 body()errorBody() 메소드가 있다. 여기에서 body는 ResponseBody가 되고 errorBody의 경우, 예를 들어 서버에서 404 코드와 에러 메시지가 포함된 에러 JSON 객체를 내려주면 String으로 받을 수 있다.

ResponseBody

public final InputStream byteStream() {
  return source().inputStream();
}

byte[] bytes() { 
  … 
  BufferedSource source = source();
  byte[] bytes;
  try {
    bytes = source.readByteArray();
  ...
}

String string() { 
  …
  BufferedSource source = source();
  try {
    Charset charset = Util.bomAwareCharset(source, charset());
    return source.readString(charset); 
  …
}

ReponseBody에서 InputStream을 가져와서 버퍼로 읽는 방법이나 byte[] 또는 String으로 서버로 받은 body를 가져올 수 있는데 String으로 binary 데이터를 읽게 되면 어떻게 될까?

String: ����JFIF��C��C��� ��    
�����+�}Yϭ�F39M>���������>���;��ˋ��uXʽ�w�ڤx\-[2g��k�S���H���m
[�V?[_W����#��v��}6�[��F�F�%����n�...

정상적인 이미지를 만들 수 없으므로 byte[]로 스트림에 있는 데이터를 가져와야 한다. 참고로 body.bytes()는 구현 코드를 살펴보면 body.source().readByteArray() 와 같은 동작이다.

body.source().readByteArray() : [-1, -40, -1, -32, 0, 16, 74, 70, 73, 70, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, -1, -37, 0, 67, 0, 16, 11, 12, 14, 12, 10, 16, 14, 13, 14, 18, 17, 16, 19, 24, 40, 26, 24, 2 … ]

이미지 Bitmap으로 변환하기

BitmapFactory의 decodeStream

InputStream input = body.byteStream();
Bitmap bitmap = BitmapFactory.decodeStream(input);

BitmapFactory의 decodeByteArray

Bitmap bitmap = BitmapFactory.decodeByteArray(body.bytes(), 0, body.bytes().length);

InputStream이나 byte array로 사용해서 쉽게 비트맵으로 이미지를 가져올 수 있다. 그리고 ResponseBody에 있는 BufferedSource에서 스트림으로 데이터를 읽기 때문에 주의해야 한다.

An elegant part of the java.io design is how streams can be layered for transformations like encryption and compression. Okio includes its own stream types called Source and Sink that work like InputStream and OutputStream – okio wiki 중에서

사실 위의 2번째 코드에서는 정상적으로 이미지를 그릴 수 없다.

byte[] bytes = body.bytes();
bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);

그 이유는 body.byte()를 호출하게 되면 스트림에 있는 데이터를 모두 읽기 때문에 다음 두 번째 호출 body.byte().length는 0이 되어 정상적인 Bitmap이 생성되지 않는다.

How to check the latest version of android support library in your SDK?

In order to the Android Support Library, we need to add the support library to dependencies in build.gradle. For example, add the following lines.

dependencies {
compile ‘com.android.support:design:25.1.0'
compile ‘com.android.support:appcompat-v7:25.1.0’
}

When do you update the support library version?

In my case, when IDE recommends to update SDK at the right top corner or when I open the SDK manager which provides the SDK tools, platforms, and others, I download and update the selected packages.

After downloaded, targetSdkVersion of defaultConfig shows the warning message with yellow underline “Not targeting the latest versions of android compatibility modes apply. consider testing this version.” for being compatible with the latest Android changes, so let’s set targetSdkVersion to 25 (Android 7.1) and run gradle build.

Finally, we will see the notice which means the support library version must be replaced to the latest version you have in your SDK and the targetSdkVersion must be the same.

How to check the latest version of android support library in your SDK?

Andorid Support Repository is local maven repository which contains all the support libraries as AAR archives from Android.

m2repository installed from dl.google.com.

As the result, you will find the latest version of support library in your local SDK. Update 25.0.1 version for com.android.support:appcompat-v7, sync project with Gradle files and run project.

[한글번역] WeChat Pay – APP端开发步骤说明

WeChat Pay App Dev Menual

원문 : APP端开发步骤说明

참고 : 아래의 번역문은 제가 직접 번역을 한 것은 아닙니다. Flitto에서 직접 한국어, 영어 번역을 참여해서 획득한 포인트로 위챗페이 개발에 필요한 문서를 번역하기 위해 번역요청(중국어->한국어)을 보내서 받은 번역문입니다. 혹시라도 위챗개발 문서가 필요한 개발자분들에게 도움이 되고자 공유합니다. 다수의 번역가가 참여했기 때문에 전체 문장이 다소 어색할 수 있음을 알려드립니다. 그리고 안드로이드 메뉴얼만 번역하였고, 4000원 비용이 들었습니다.

Android 개발 요점 설명

1. 백그라운드 설정

위챗 오픈플랫폼에서 개발 어플리케이션을 신청하시면 위챗 오픈플랫폼에서는 어플리케이션의 고유 식별 아이디를 생성해 드립니다. 안전한 결제를 위해 오픈플랫폼에 업체의 어플리케이션 패키지 이름과 어플리케이션 서명(signature)을 연동시키셔야 합니다. 연동 후에 정상적으로 결제가 가능합니다. 설정화면은 <오픈플랫폼>의 <관리센터/어플리케이션 수정/개발데이터 수정>메뉴에 있으며 그림 8.8의 빨간 네모로 표시되어 있는 부분입니다.

8.8

패키지명: 어플리케이션 프로젝트의 AndroidManifest.xml에 설정한 package 이름값이다. 예를들어 DEMO에서 package값은 net.sourceforge.simcpux.

어플리케이션 서명: 프로젝트의 패키지명 및 컴파일에 사용된 keystore을 signing툴을 사용하여 생성한 32비트길이의 MD5문자열입니다. 테스트기기에 signing툴을 설치하고 실행하면 application의 서명을 생성합니다. 그림8.9와 같이 녹색문자열이 곧 어플리케이션의 서명이다. signing툴 다운로드 url https://open.weixin.qq.com/zh_CN/htmledition/res/dev/download/sdk/Gen_Signature_Android.apk

8.9

2. APP ID 등록하기

WeChat jar패킷을 개발자의 앱프로젝트로 가져옵니다. 하지만, API를 사용하려면 우선 위챗으로 APPID를 등록해야 합니다. 코드는 아래와 같습니다:

final IWXAPI msgApi = WXAPIFactory.createWXAPI(context, null);
// app id를 위챗에 등록하기
msgApi.registerApp("wxd930ea5d5a258f4f");

3. 결제 호출

업체서버로부터 결제 청구를 생성합니다. 먼저 통합구매 API를 호출해(자세한 내용은 7장 참조) 구매리스트를 생성 후, prepay_id 값을 받아서 다시 사인 후 앱에 전송하여 결제 청구를 제출한다. 아래는 웨이신 결제를 호출하는 메인 코드입니다.

IWXAPI api;
PayReq request = new PayReq();
request.appId = "wxd930ea5d5a258f4f";
request.partnerId = "1900000109";
request.prepayId= "1101000000140415649af9fc314aa427",;
request.packageValue = "Sign=WXPay";
request.nonceStr= "1101000000140429eb40476f8896f4c9";
request.timeStamp= "1398746574";
request.sign= "7FFECB600D7157C5AA49810D2D8F28BC2811827B";
api.sendReq(req);

주의: sign 필드명으로 생성되는 리스트는 API 발급 설정을 참고하세요.

4. 지불결과 콜백

위쳇 SDK Sample을 참조하여, net.sourceforge.simcpux.wxapi 패캣경로에서 WXPayEntryActivity클래스를 생성합니다.(패킷명 혹은 클래스명이 일치하지 않으면 콜백이 불가능합니다), WXPayEntryActivity클래스에서 onResp 함수 구현, 지불완료 후 위쳇 엡은 판매자 어플리케이션으로 돌아가면서 onResp 함수를 콜백합니다. 개발자는 해당 함수내에서 알림메세지를 받아야하며 기존으로 돌아가기 위한 에러코드를 판단해야 합니다. 지불 성공 후 백오피스로 가서 지불결과를 체크한 후, 유저의 실제 지불결과를 보여줍니다. 클라이언트의 복귀를 유저 결제결과로 적용하지 않도록 꼭 주의해야 합니다. 서버사이드에서 받은 결제 알림메세지 혹은 API복귀결과를 체크하여 기준으로 해야 합니다, 코드예시는 다음과 같습니다:

@Override
public void onResp(BaseResp resp) {
if (resp.getType() == ConstantsAPI.COMMAND_PAY_BY_WX){
Log.d(TAG,"onPayFinish,errCode=" + resp.errCode);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.app_tip);
}
}

콜백한 errCode 리스트:

명칭 기술 솔루션
0 성공 성공화면
-1 오류 발생가능한이유 : 사인(sign)오류, APPID 미등록, 프로젝트상에서 APPID설정오류, 등록된APPID와 세팅한것 맞지 않는 경우, 그 외
-2 사용자취소 발생가능한이유 : 사용자취소버튼 누르고 APP으로 복귀

ListView의 addHeaderView(Footer)를 RecyclerView 에서 구현하기

안드로이드 앱 개발시 가장 많이 사용하던 위젯은 ListView이다. 그리고 RecyclerView는 Android 5 Lollipop 버전이 업데이트 되었을 때, Support Library V7에 추가되었고 이를 사용하기 위해서는 build.gradle에 다음과 같이 추가하면 사용할 수 있다.

compile 'com.android.support:recyclerview-v7:${supportLibraryVersion}'

기존에 ListView에서 사용하던 상, 하단에 뷰 추가를 지원하던 addHeaderViewaddFooterView가 RecyclerView에서는 더이상 지원하지 않는다. 그래서 대부분의 안드로이드 개발자들은 StackOverFlow에 ‘How to add header view to recyclerview’ 다음과 같이 검색했을 것이다. 이미 RecyclerView에 HeaderView나 FooterView를 추가해주는 오픈 소스는 많이 공개되어 있다. 필요하다면 찾아서 사용하면 쉽게 뷰를 상하단에 추가할 수 있다. 결과적으로 기존 ListView에서는 addHeaderView가 어떻게 동작 했으며 RecyclerView에서는 그 기능을 동작하도록 할 수 있는지 한번 알아보려고 한다.

HeaderViewListAdapter (android.widget)
public void addHeaderView(View v, Object data, boolean isSelectable) {
    ...
    // Wrap the adapter if it wasn't already wrapped.
    if (mAdapter != null) {
        if (!(mAdapter instanceof HeaderViewListAdapter)) {
            mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
        }
    …
    }
}

ListView의 addHeaderView의 내부를 들여다 보면 기존의 ListAdapter 대신에 HeaderViewListAdapter 어댑터 클래스를 사용하며 이 위젯은 래핑에 필요한 WrapperListAdapter 인터페이스를 상속하고 있으며 Adapter를 가지고 있는 껍데기가 된다. 기존의 Adapter를 HeaderViewListAdapter로 재생성(Wrapping)하면서 mAdapter를 파라미터로 넘겨주는 이유가 바로 이 때문이다. 따라서 HeaderViewListAdapter에서는 기존의 Adapter를 그대로 가지고 있으면서 화면에 상하단에 추가된 뷰를 노출시켜주는 함수를 더 가지고 있다. 다음으로 getView함수를 살펴보자.

public View getView(int position, View convertView, ViewGroup parent) {
    // Header (negative positions will throw an IndexOutOfBoundsException)
    int numHeaders = getHeadersCount();
    if (position < numHeaders) {
        return mHeaderViewInfos.get(position).view;
    }

    // Adapter
    final int adjPosition = position - numHeaders;
    int adapterCount = 0;
    if (mAdapter != null) {
        adapterCount = mAdapter.getCount();
        if (adjPosition < adapterCount) {
            return mAdapter.getView(adjPosition, convertView, parent);
        }
    }

    // Footer (off-limits positions will throw an IndexOutOfBoundsException)
    return mFooterViewInfos.get(adjPosition - adapterCount).view;
}

Header로 추가된 상단뷰의 수보다 position이 작은 경우에는 HeaderView를 리턴해주고 ListView에서 Row Item의 position은 Header 수 만큼 빼서 그려준다. 상하단에 추가된 뷰가 있을 경우, 그것을 보여주고 그게 아니라면 Adapter의 기본 로직으로 동작하게 된다.

HeaderRecyclerViewAdapter 구현하기
public abstract class HeaderRecyclerViewAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> {

개발 중인 서비스에서 다양한 데이터 타입이나 뷰를 사용 한다면 일일이 대응되는 클래스(RecyclerView.Adapter 상속 받는)를 생성하기 보다는 HeaderRecyclerView를 사용하거나 구현 방법을 한번 보는 것을 추천한다. 목록을 보여주는 ListView와 RecyclerView는 기본 개념이 같이 때문에 내부 동작 또한 크게 차이는 없을 것이다.

달라진 것이 있다면 ListView에서 종종 findViewById의 비용을 줄이기 위해 사용하던 ViewHolder가 RecyclerView.ViewHolder로 추가되어 예전에는 Adapter에서 getView를 통해 View를 생성했다면 이제는 ViewHolder를 생성하고(onCreateViewHolder) 데이터 바인딩할 때(onBindViewHolder), 파라미터로 다시 받아 사용할 수 있다.

public class HeaderRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{

  private int TYPE_HEADER = -1;
  private int TYPE_ROW_ITEM = -2;

…
  public void addHeaderView(View header) {
    mHeaderViews.add(header);
  }

  @Override
  public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    if (isHeaderPosition(position)) {
      return new ViewHolder(mHeaderViews.get(position));
    } else {
      return mInnerAdapter.onCreateViewHolder(parent, position);
    }
  }

  @Override
  public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (isHeaderPosition(position)) {
      //  bind HeaderViewHolder
    } else {
      //  bind ItemViewHolder
    }
  }

  @Override
  public int getItemViewType(int position) {
    // return 3 item type by position
  }
…

ListView의 addHeaderView 함수를 HeaderRecyclerViewAdapter에 추가한다. 그리고 RecyclerView.Adapter가 position과 ViewHolder의 타입에 따라 동작해야 하므로 위의 3가지의 함수가 동작에 있어서 가장 중요하다. 마찬가지로 만약에 FooterView를 RecyclerView에 추가하고 싶을 경우, 뷰의 식별자를 선언하고 ViewHolder를 생성하는 부분과 데이터를 바인딩하는 부분에 추가한다. 그리고 getItemViewType에서 화면상에 FooterView가 노출되는 position일 때 리턴해주면 된다. 마지막으로 앞으로 자주 사용할 RecyclerView와 많이 사용했던 ListView를 자유롭게 다뤄보자.

Proguard 난독화와 Crash Report Tool로 이슈 디버깅

프로젝트를 운영(유지보수)하면서 가장 중요하면서도 가장 기본이 되는 작업이 버그 리포트이다. 여기에선 Crash Report Tool로 Crashlytics를 사용했다. 그리고 회사 또는 팀의 정책, 작게는 개인에 따라 코드의 중요성이 높다면 난독화를 고민하게 되고 안드로이드에서는 무료인 Proguard 적용을 고려해보게 된다. (코드의 중요성이 낮더라도 한번 경험해보는 것도 좋다고 생각함) 하지만 난독화된 앱을 배포한 후 발생한 이슈를 디버깅하기 위해 Crash Report 콘솔에서 확인하면 이상한 문자를 접하게 된다.

Proguard 적용 전 이슈 발생했을 때

난독화가 되지 않은 앱을 디컴파일 해보면 소스 코드가 정상적으로 확인할 수 있다. 포함된 Android Support Library도 난독화가 되지 않은 상태이다.

그리고 앱에서 이슈를 발생시켜 보면 특정 함수에서 발생했으며 어떤 파일의 몇 번째 줄에서 발생했는데 ‘(MainActivity.java:21)을 통해서 알 수 있다.

public void forceCrash(View view) {
    throw new RuntimeException("This is a crash");
}

이슈는 다음과 같이 RuntimeException을 버튼 클릭 때 발생시키도록 만들었다.

Proguard 적용하기

프로젝트 생성 시 기본적으로 다음과 같이 build.gradle파일에 빌드 설정으로 다음과 같이 된다.

buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

그리고 배포 시 Proguard 적용하기 위해서는 minifyEnabled를 true로 변경하면 된다.

buildTypes {
    release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

ProGuard는 릴리스 모드(release)에서 빌드할 때는 적용되지만, 디버그 모드(debug)에서는 적용되지 않는다.

proguard를 적용해서 Signed APK파일을 만들고 디컴파일 했을 경우에는 위처럼 패키지부터 소스 파일까지 모두 특정 알파벳으로 난독화가 된 것을 확인할 수 있다.

앱에서 이슈를 발생시켜서 Crashlytics 콘솔에서 확인해보면 위에서 확인했던 이슈가 발생한 파일과 라인 수 부분이 Unknown Source로 변경되었다. 간단한 샘플 프로젝트이기 때문에 코드로 디버깅이 가능하지만 큰 규모의 프로젝트에서는 디버깅하는데 어려움을 겪을 수 있다.

그래서 proguard-rules.pro에 Proguard Rule을 추가하자.

-keepattributes SourceFile,LineNumberTable

다시 Proguard가 적용된 APK파일 생성하고 이슈를 발생 시켜보자.

Crashlytics를 확인하면 Unknown Source 대신에 이슈가 발생한 파일과 라인 수를 확인할 수 있다. 그리고 출력된 stack trace 오류에서 난독화된 패키지가 무엇인지 알고 싶다면 /outputs/mapping/release/mapping.txt 파일을 열어보자.

android.support.v4.app.Fragment -> android.support.v4.a.t:
    android.support.v4.util.SimpleArrayMap sClassMap -> aa
    java.lang.Object USE_DEFAULT_TRANSITION -> a
    int mState -> b
    android.view.View mAnimatingAway -> c
    int mStateAfterAnimating -> d
    android.os.Bundle mSavedFragmentState -> e
    android.util.SparseArray mSavedViewState -> f
    int mIndex -> g
    java.lang.String mWho -> h

Fragment가 속한 패키지는 android.support.v4.a.t로 변경된 것을 확인할 수 있으며 이에 속해 있는 변수나 함수들이 어떻게 변경되었는지 또한 확인할 수 있다. 난독화를 했다면 mapping.txt 파일은 보관해두고 만약 난독화된 코드의 stack trace를 디코딩하고 싶다면 mapping.txt를 이용해서 가능하기 때문이다.

retrace.bat -verbose mapping.txt stacktrace.txt > out.txt

out.txt파일에는 난독화된 코드의 stacktrace.txt가 해석된 stack trace가 출력된다.

ProGuard outputs the following files after it runs:

dump.txt
Describes the internal structure of all the class files in the .apk file
mapping.txt
Lists the mapping between the original and obfuscated class, method, and field names. This file is important when you receive a bug report from a release build, because it translates the obfuscated stack trace back to the original class, method, and member names. See Decoding Obfuscated Stack Traces for more information.
seeds.txt
Lists the classes and members that are not obfuscated
usage.txt
Lists the code that was stripped from the .apk

Proguard 사용하는 규칙

Proguard Manual – Proguard 사용 메뉴얼

-dontwarn [class_filter]
Specifies not to warn about unresolved references and other important problems at all. The optional filter is a regular expression; ProGuard doesn’t print warnings about classes with matching names. Ignoring warnings can be dangerous.
keep [,modifier,…] class_specification
Specifies classes and class members (fields and methods) to be preserved as entry points to your code.
-keepattributes [attribute_filter]
Specifies any optional attributes to be preserved. The attributes can be specified with one or more -keepattributes directives. The optional filter is a comma-separated list of attribute names that Java virtual machines and ProGuard support. Attribute names can contain ?, *, and ** wildcards, and they can be preceded by the ! negator.
-assumenosideeffects class_specification
Specifies methods that don’t have any side effects (other than maybe returning a value). In the optimization step, ProGuard will then remove calls to such methods, if it can determine that the return values aren’t used. With some care, you can also use the option to remove logging code.

Proguard를 적용하고 Signed Apk를 생성할 때, 가끔 발생하는 이슈들이 있는데, 그 중 하나가 Referenced Class를 찾을 수 없어서 IOException이 발생했기 때문이죠.

오류 메시지를 보면 Please correct the above warnings first로 발생한 Warning들을 고쳐달라고 합니다.

Proguard 적용 중에 왜 Warning이 발생했는지 궁금하시면 Problem while processing에서 확인할 수 있어요. 이때 위에 있는 규칙들 중에서 맞는 규칙을 찾아 proguard-rules.pro에 추가해준다.

-dontwarn okio.**

위의 이슈는 okio 패키지에 있는 파일들에 대해선 Warning을 뜨지 않도록 다음과 같이 설정을 해주면 된다.

-keep class com.example.classname
-keepattributes attribute

# examples
-keep class android.support.v4.app.** { *; }
-keep interface android.support.v4.app.** { *; }
-keep class android.support.v7.** { *; }
-keep interface android.support.v7.** { *; }

만약에 Class 변환 중에 이슈가 발생하거나 외부 라이브러리의 Proguard 적용을 원하지 않을 경우, 해당 패키지의 파일들을 Proguard가 적용되지 않도록 설정이 가능하다. 구글 라이브러리의 경우, Proguard가 불필요하기 때문에 Proguard적용에서 제외시켜주는게 좋다. 그리고 보통은 외부 라이브러리를 추가할 때, Proguard 적용시 주의사항으로 규칙들을 제공해주고 있으므로 잘 확인해서 추가만 해준다면 크게 문제가 발생하지 않는다.

decompile apk (APK 디컴파일하기)

가끔 진행하는 앱의 코드 난독화 확인을 위해서 기록해 두려고 한다. APK 디컴파일(Decompile)을 악용하지 말았으면 한다. 안드로이드 애플리케이션이 컴파일되면 .dex 파일이 생성된다. dex는 Dalvik Executable로 Dalvik에서 실행할 수 있는 파일이며 컴파일된 코드 파일을 의미한다.

Download dex2jar tool

1. dex파일을 jar(.class files)로 변환하기 위해서 dex2jar를 다운로드 한다.

2. Signed APK 파일의 확장명을 apk에서 zip으로 변경한다.

3. zip 압축을 풀면 classes.dex 파일을 다운받은 dex2jar 폴더로 복사한다.

$ ./d2j-dex2jar.sh classes.dex
sh: ./d2j-dex2jar.sh: Permission denied

4. 위처럼 dex2jar.sh 실행하면 권한 거부로 실행할 수 없게 된다.

$ chmod 764 *.sh
$ ./d2j-dex2jar.sh classes.dex
dex2jar classes.dex -> ./classes-dex2jar.jar

실행 권한을 변경한 후 다시 실행하면 classes-dex2jar.jar 파일이 생성된다.

Download JD_GUI

JD(Java Decompiler)-GUIjar파일 안에 있는 class파일들을 보기 위해서 다운로드 한다.

생성된 classes-dex2jar.jar를 열면 모든 class파일들을 볼 수 있다.

CachePot으로 Activity 또는 Fragment 사이 간단한 데이터 통신

Java에서 흔히 객체로 데이터 통신 하기 위해 객체에 Serializable 인터페이스를 상속 받은 뒤 직렬화된 객체를 바이트 단위로 분해하여 전송한다. Android에서도 마찬가지로 Serializable을 사용하긴 하지만 보다 Parcelable을 많이 사용하고 있다. 하지만 Parcelable을 사용하기 위해서는 객체에 선언된 데이터가 추가 될 때마다 writeToParcelreadFromParcel을 통해 Parcel 객체에 읽고 쓰는 작업을 추가해야 데이터 전송이 가능하다. CachePot은 간단한 앱을 만들거나 Intent를 통한 다른 애플리케이션과 통신이 없다면 간단하게 데이터 캐시를 할 수 있는 인스턴스가 있으면 좋겠다고 생각되어 Generic을 사용해 간단하게 만든 안드로이드 라이브러리이다.

Download

현재 Gradle을 사용하는 중이라면 build.gradle에 아래를 추가하자.

repositories {
      jcenter()
}

dependencies {
      compile 'com.github.kimkevin:cachepot:1.0.0'
}

How To Use?

동기식 데이터 전달하기

  • Between Activity and A ctivity
  • Between Activity and Fragment
  • Between Fragment and Fragment

데이터가 메모리에 올라가기 때문에 결국은 사용자 인테페이스를 구성하는 Activity와 Fragment 등 아무대서나 동기적으로 데이터 전달이 가능하다.

1. Model 객체 전달

새로운 화면 생성 전 데이터 저장하기

KoreanFood foodItem = new KoreanFood(1, "Kimchi", "Traditional fermented Korean side dish made of vegetables");
CachePot.getInstance().push(foodItem);

새로운 화면 생성 후 데이터 가져오기

public class MainFragment extends Fragment{
    private KoreanFood foodItem;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        foodItem = CachePot.getInstance().pop(KoreanFood.class);
    }
}

2. Collection 또는 Map 객체 전달

새로운 화면 생성 전 데이터 저장하기

List<KoreanFood> foodItems = new ArrayList<>();
foodItems.add(new KoreanFood(1, "Kimchi", "Traditional fermented Korean side dish made of vegetables"));
foodItems.add(new KoreanFood(2, "Kkakdugi", "A variety of kimchi in Korean cuisine"));
CachePot.getInstance().push(foodItems);

새로운 화면 생성 후 데이터 가져오기

List<KoreanFood> foodItems = CachePot.getInstance().pop(ArrayList.class);

ArrayList나 HashMap, Stack, LinkedList 등과 같이 구현 클래스도 일반적인 모델 데이터를 전달하는 것과 같은 방식으로 데이터 전송이 가능하다.

비동기식 데이터 전달하기

ViewPager에서 position별로 Fragment에 객체 전달

요즘 앱에서 흔하게 ViewPagerFragmentStatePagerAdapter를 사용하고 있다. 그리고 Adapter에서는 getItem(int position)이 호출 될 때 Fragment를 생성하게 되는데 새로운 Fragment가 생성되고 CachePot 인스턴스에 저장된 데이터를 가져오기 전에 새로운 데이터를 캐시하게 되어 데이터를 가져올 때 오류가 발생하게 된다. 이를 방지하기 위해서 Fragment의 postiion을 함께 저장할 수 있는 기능을 제공한다.

Framgnet 생성 전에 position과 데이터 저장하기

private class PagerAdapter extends FragmentStatePagerAdapter {
    ...
    public Fragment getItem(int position) {
        CachePot.getInstance().push(position, foodItems.get(position));
        return FoodFragment.newInstance(position);
    }
}

생성된 Fragment에서 데이터 가져오기

public static FoodFragment newInstance(int position) {
    FoodFragment fragment = new FoodFragment();
    Bundle args = new Bundle();
    args.putInt(ARG_POSITION, position);
    fragment.setArguments(args);
    return fragment;
}
...
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (getArguments() != null) {
        final int position = getArguments().getInt(ARG_POSITION);
        koreanFoodItem = CachePot.getInstance().pop(position);
    }
}

제3회 부산모바일앱공모전 대상

부산모바일앱공모전?

부산지역 모바일 앱분야 우수개발자를 발굴, 육성함으로써 지역 모바일 앱산업 및 창업 활성화를 도모, 부산시민을 대상으로 편의성 제공 및 부산경제에 도움이 되는 공공 서비스용 앱 개발을 위한 매년 개최되는 모바일 어플리케이션 공모전이다.

작년에 참가 했던 제3회 부산모바일앱공모전에서 “터틀넥 목디스크” 라는 애플리케이션으로 대상이라는 큰 상이 나에게 왔다. 상금도 작은 금액이 아니었기에 사실 너무 과분하다는 생각이 들기도 했다. 공모전 참여 과정은 프로젝트 신청서를 먼저 제출하고, 개발 한 뒤에 결과물을 홈페이지에 등록하면, 심사 기간을 거쳐 공지된다. 공지된 사람들에 한에서 IT 전문가들 6~7명 정도 앞에서 작품에 대한 프리젠테이션을 해야되고, 최종적으로 심사 발표가 난다. 지금 돌이켜 생각해보면 많은 IT전문가들 앞이라 대답할 때, 그 만큼 긴장을 했기에 대답을 시원하게 하지는 못했기 때문에 상을 받기 어려울 것 같다는 생각을 했었다. 지금 생각나는 질문은 “배터리 문제는 어떻게 생각하시나요?”, “올바른 스마트폰 자세의 의학적 근거는 있나요?” 등 30 ~ 40분 가량 equipment 하나같이 폭풍같은 질문이었던 것으로 기억한다.

목디스크 앱을 만든 이유는?

목디스크를 관련한 아이디어로 앱을 만들 수 있었던건, 학교 가던 버스안에서 고개 숙여 스마트폰을 뚫어져라 보던 사람들, 우연히 뉴스 기사로 읽었던 스마트폰으로 인한 목디스크의 심각성을 알게 되면서 ‘어떻게 하면 저 사람들의 고개를 덜 숙이게 할 수 있을까?’ 라는 문제를 제기하게 되었고, 해결 방법으로 스마트폰의 기울기 센서를 이용했다. 사실 그 땐, 기울기 센서를 이용한 게임이 많이 출시되고 있었고, 당시 게임 개발을 하고 있었기 때문에, 이것을 이용하면 사람들의 자세 교정에 도움이 될 것 같다는 생각을 했다. 사실 우연히 발견한 작은 문제와 호기심으로 시작했고 이렇게 큰 결과를 가질 것이라고 예상을 하지 못했다.

부산 시민 모두가 참여할 수 있는 대화라 많은 분들이 팀 단위로 공모전에 참여했었고, 발표를 대기할 때 혼자라 외롭기도 했었다. 팀으로 참여를 한 것이 아니기에 혼자 기획, 개발, 디자인할 때는 많이 힘들었지만 지나고 나면 좋은 추억이 되듯이 개발자로 성장하는데 좋은 믿거름이 될 것 같다. 우리 주위에 찾아보면 항상 좋은 기회들은 많이 있고, 얼마만큼 자신이 하고 싶고 즐길 수 있느냐 잘 생각해본다면, 결과는 나쁘더라도 좋은 경험이 Beautiful 될 것같다. 그리고 마지막으로 이렇게 큰 상을 주신 부산광역시와 부산정보산업진흥원에 감사합니다.

미디어

“제3회 부산 모바일 앱 공모전 수상작” 부산정보산업진흥원
“부산 모바일 앱 공모전 ‘터틀넥- 자라목’ 대상” 부산일보
“부산 모바일 앱 공모전 ‘자라목’ 대상” 국제신문
“터틀넥 등 18편 선정 부산모바일앱 공모전” 전자신문

[Android] JNI – java.lang.UnsatisfiedLinkError

 java.lang.UnsatisfiedLinkError



 오랫만에 JNI를 사용하던 SA3D 도중에 [Android] 컴파일을 cheap nfl jerseys 하고 난 후 에러가 발생했습니다. 라이브러리 호출을 담당하는 System.loadLibrary 메소드 호출 시 발생하는 에러네요. 이때 이 에러는 라이브러리를 찾지 못할 때 Türk 발생한다. wholesale mlb jerseys  그래서 먼저 경로 확인을 [Android/Error] 했더니 경로는 이상이 Ima 없었고 다름 아닌 cheap jerseys from China 함수의 이름이었다.

 그러나! 함수 이름을 수정한 후에 cheap jerseys 다시 컴파일을 하고 ring 실행을 했더니 다시 에러가 발생했다. 이 문제는 이렇게 해결! JNI를 컴파일한 후 생성되는 폴더(obj)를 삭제하고 다시 컴파일을 하면 된다.