pinkred's mobile program

pinkred mobile programer

Posts Tagged ‘Android

ConstraintLayout 쉽게 개발하기

leave a comment »

ConstraintLayout을 들어보지 못한 안드로이드 개발자가 없을 것이다. 하지만 적용해보았는지 물어보면 대부분 어렵다거나 혹은 버그가 많다거나 해서 쉽게 적용하지 못한 것으로 판단된다. 물론 잘 적용하시고 계신 분들도 많으리라 생각됩니다.

그럼 왜 적용하기 어려울까? 대부분 한글로 쉽게 설명된 자료가 없을 것이다. 물론 많은 관련 자료는 있지만 대부분 레퍼런스 자료 이거나 영문으로 되어있어서 쉽게 이해를 하지 못하기도 한다.

ConstraintLayout은 RelativeLayout의 확장형이라고 생각된다. 물론 성능은 더 좋아지고 쉽게 개발할수 있도록 해준다. 가 구글에서 말하는 거지만 실제 적용하려고 하면 다들 힘들어한다. 몇가지 개념을 알고 접근해야 한다. 어려운 용어는 버리고 쉬운 용어로 접근해 보도록 하겠다.

다들 아시겠지만 layout 구성시 가장 중요한 것은 깊이를 만들지 않는 것이고 그것을 도와줄수 있도록 ConstraintLayout이 많은 부분을 지원한다.

  1. 기존에는 match, wrap, dp로 width, height를 정의 했을 것이다. 이부분이 많이 달라졌다. 기존에는 match로 하면 자동으로 부모의 넓이를 가지게 되었다.  그리고 margin은 부모의 넓이를 기준으로 동작하였다. ConstraintLayout에서는 해당 정의가 약간 바뀌었다.

위의 이해가 가장 어려운데 이제는 부모가 중심이 아니고 나를 중심으로 한다. 예를 들어서 match 로 width를 정의하려고 하면

 

width = 0dp

layout_constraintLeft_toLeftOf = “parent”

layout_constraintRight_toRightOf = “parent”

위와 같은 방식으로 width를 0으로 주고 좌, 우를 부모를 중심으로 한다. 물론 부모인 ConstraintLayout는 match_parent로 되어있어야 한다. 이러한 방식으로 left, right, top, bottom을 설정하게 된다. 또한 ConstraintLayout에 있는 child는 꼭 left, right 와 top, bottom이 설정되어있어야 한다. 무슨 말이야 하면 left, right 값중에 한가지 꼭 left_toLeftOf가 아니더라고 기준이 될수 있는 left, right 값이 꼭 있어야하고, top, bottom도 둘중에 기준이 되는 값은 꼭 있어야 한다. 이 기준값을 넣어주지 않으면 Android 4.x에서 제대로 보여지지 않거나 원하지 않는 모습이 나올수도 있다.  정말 중요하다.

예를 들어서 위의 예제에서 Top, Bottom의 기준이 없지만 아마 xml에서는 그냥 자동으로 Top으로 보이것이다. 하지만 속지 말아야 한다. 제대로 동작하지 않을수 있다. 그러므로 상단으로 부터 시작한다면 layout_constraintTop_toTopOf = “parent”를 꼭 넣어주어야 한다. parent가 아니어도 상관없으니 꼭 넣도록 하자.

그럼 width, height의 기준을 잡아주는 내용을 살펴 보도록 하자.

layout_constraintLeft_toLeftOf

왼쪽의 기준은 어떻게 잡을지 결졍한다. “parent”를 선택하면 부모와 동일한 왼쪽 기준이 되고 다른  view를 선택하면 해당 view의 왼쪽 선과 동일하게 된다.
layout_constraintLeft_toRightOf

왼쪽 뷰의 오른쪽에 위치한다. 왼쪽에 뷰가 있다면 그 뷰의 오른쪽에 위치한다.
layout_constraintRight_toLeftOf

오른쪽 뷰의 왼쪽에 위치한다. 오른쪽에 뷰가 있다면 그 뷰의 왼쪽에 위치한다.
layout_constraintRight_toRightOf

오른쪽의 기준은 어떻게 잡을지 결졍한다. “parent”를 선택하면 부모와 동일한 오른쪽 기준이 되고 다른  view를 선택하면 해당 view의 오른쪽 선과 동일하게 된다.
layout_constraintTop_toTopOf

상단의 기준은 어떻게 잡을지 결졍한다. “parent”를 선택하면 부모와 동일한 상단 기준이 되고 다른  view를 선택하면 해당 view의 상단 선과 동일하게 된다.
layout_constraintTop_toBottomOf

상단 뷰의 하단에 위치한다. 상단에 뷰가 있다면 그 뷰의 하단에 위치한다.
layout_constraintBottom_toTopOf

하단 뷰의 상단에 위치한다. 하단에 뷰가 있다면 그 뷰의 상단에 위치한다.
layout_constraintBottom_toBottomOf

하단의 기준은 어떻게 잡을지 결졍한다. “parent”를 선택하면 부모와 동일한 하단 기준이 되고 다른  view를 선택하면 해당 view의 하단 선과 동일하게 된다.
layout_constraintBaseline_toBaselineOf
baseline을 가지고 있는 뷰와  baseline을 일치시킨다.(ex. TextView)

 

위의 기준을 잡아주면 기본적이 배치는 끝났다. 디자인 툴을 사용하여 연결해도 좋은데 직접 코딩해서 해당 기준선들을 넣어주는 것들이 나중에는 더 편하다. 왜냐하면 디자인 툴을 이용하면 드래그로 연결하는 것도 힘들고 실제 값이 들어있지 않는 경우에는 폭이나 높이가 좁아서 연결하기도 힘들다. 그래서 옆에 preview을 띄워 높고 직접 코딩으로 해당하는 값들을 넣어주는 것이 오히려 더 편했다.

ConstraintLayout을 변경시키려고 무작정 부모 Layout을 변경하면 자동 툴 변화로 자식 값의 위치값이 강제로 할다되는 경우가 발생하기 때문에 원본을 따로 복사해두고 하나씩 추가하는 방식으로 변경하는 것이 좋고 match값이 있는 경우에는 0dp로 우선 바꾸어 주고 copy and paste 해주는 것이 값이 변경되지 않는다.

ConstraintLayout의 기준을 설정되면 해당 기준으로 margin값을 정의 할 수 있다. 무슨 말이냐하면 기준값을 left만 넣게 되면 marin_right값을 정의 하더라도 반영이 되지 않는다. 무조건 기준이 되는 값을 넣고 그에 해당하는 marin값을 넣어야 반영된다.  기준값에 left, top이 정의되어있으면 margin은 left, top 만 가능하다는 말이다.

다음 시간에는 view 간의 연결 방식에 대해서 설명하겠습니다.

Written by pinkredmobile

2017/10/02 at 5:09 pm

Firebase A/B Test 하기

leave a comment »

A/B테스트를 할수 있는 플랫폼은 많이 있다.  회사마다 다양항 방식으로 진행하겠지만 Firebase를 이용해서 진행하는 방식을 해보기로 하겠다.

Firebase A/B Test는 Remote Config를 통해서 진행할수 있다.  구글개발자문서

Firebase가 제공하는 것은 A,B 인지를 제공하는 정도이다.  실망이 많을수 있다. 그럼 분석은 어떻게? 분석은 내부에서 사용하고 있는 것으로 사용해야 한다.

여기서 소개하는 것은 A/B를 제공하는 방식을 어떻게 Firebase 를 통해서 받을수 있을까에 대해서 소개하도록 한다.

스크린샷 2017-09-30 오후 5.49.15

A/B 테스트는 Remote Config의 CONDITIONS을 통해서 진행된다. 대략적인 진행 방식은 아래와 같다.

  1.  내가 테스트 하고 싶은 조건을 선택한다.
  2. 해당 조건으로 A, B인지의 조건을 부여한다.
  3. 앱에서 Remote Config를 통해서 A,B인지에 따라서 관련된 코딩을 진행한다.
  4. 해당 코딩에 분석툴을 추가하여 A, B의 결과를 분석한다.

Remote Config는 1,2번을 할수 있도록 지원한다.

조건은

스크린샷 2017-09-30 오후 5.57.18

  1. 앱은 Remote Config 에는 여러가지 앱을 등록할수 있다. 해당 앱중에 한가지를 선택한다.
  2. 앱을 선택하면 버전을 넣을수 있는데 해당 앱의 버전을 여러가지 조건으로 거를수 있다.
  3. 운영체제도 선택 가능한다. iOS, Android,

 

아래 조건은 주로 사용하는 부분은 임의 백분위수의 사용자입니다.

스크린샷 2017-09-30 오후 5.55.33

 

Remote Config에서 조건을 선택하는 방식은 크게 2가지 방식이 있다.

  1. 모든 조건을 한가지에 거는 방식
  2. 조건을 하나씩 선택해서 거르는 방식.

내 무슨 말인지 모르겠다구요. 설명 드리도록 하겠습니다. 해당 내용은 저도 너무 궁금해서 구글 개발자지원을 받아서 알아낸 것입니다.

예시로 테스트 조건을

OS : Android

버전 : 1.0

모수 : A : 10%, B : 10, 나머지는 기본으로

  1. 모든 조건을 한가지에 거는 방식은 아래와 같이 모든 조건을 걸어서 테스트 하는 방식이다.

스크린샷 2017-09-30 오후 6.06.40

스크린샷 2017-09-30 오후 6.07.19

참고로 B의 조건이 50~60으로 되어있는데 이렇게 한 이유는 추후에  A,B조건의 모수를 변경시에 유효범위를 위해서 입니다. 이렇게 하면 A는 0 ~50%가 유효범위고  B는 50~100% 가 유효범위이다.

2. 조건을 하나씩 거르는 방법은 아래와 같이 위에서 부터 조건을 거르는 것이다.

ios 유저를 처음에 거르면 안드로이드 유저만 남는다.

다음에 버전이 1.0이 아닌 사람을 거르면 1.0만 남는다.

그후에 A : 0~10,  B : 50~60 으로 나눈다.

스크린샷 2017-09-30 오후 6.20.19

위의 2가지 중에 어는 것이 효율적 일까 구글에 물어보니 아래의 방식이 조건이 더 효율적이라고 한다. 간단히 정리하자면 위의 방식의 조건의 개수가 아래 방식보다 많기 때문이다. 어차피 조건은 구글에서 만들어 처리해주는 것이니 아무거나 사용이 가능한데요. 아래와 같은 방식으로 하면 추후에 확장시(조건 변경)에 조금더 편리합니다

한번 조건에 걸린 유저는 앱을 삭제할때 까지 유지 된다. 재설치후에 변경될수 있다.

Remote Config를 통해서 A, B인지를 받아서 나머지 부분은 열심히 코딩하고 분석 툴을 넣어주면 된다. 물론 어떤 플랫폼은 분석까지 제공해주지만 기본적으로는 A,B를 제공해주는 것으로 만족해하고 있다. 여태까지 테스트를 하면서 가장 큰 이슈는 정말 A,B 를 잘 나누어 주는지였는데 난 구글을 믿고 싶었지만 대부분의 사람들이 신뢰를 하지 않았다. 몇번의 테스트를 통해서 모수를 잘나누어 준다고 결과가 리포트 되었고 이후로는 관련 이슈는 없었다.

Written by pinkredmobile

2017/09/30 at 6:31 pm

Android MVP pattern, Clean Architecture more

leave a comment »

개발을 하다보면 정형화된 틀을 생각하게 된다. 이는 혼자서 개발하든 협업을 하던지 이러한 틀이라는 것을 고민하게 된다. 한동안 안드로이드에서는 MVP 패턴에 대해서 많은 이야기들이 있었고, 구글에서는 기본적인 제안(Android Architecture Components)를 하기 시작했다.  이 글은 아직 구글이 제시하는 안 이전에 구현된 것으로 추후에는 안드로이드가 제공하는 컴포넌트를 연동하게 된다면 다시 한번 그후에 글을 써보도록 하겠다.

현제 추세가 MVP  패턴에 대해서 이야기 하고 있고 MVVM 패턴으로 넘어가기 시작 단계에 있다고 생각된다. 물론 MVP 패턴을 적용하더라도 큰 이슈는 없으며 전체적인 바향은 의존성(Dependence)을 제거하기 위한 방법으로 진행되고 있다고 생각된다.

Clean Architecture를 도입하고 시작한 것은 결국 패턴의 이슈로는 의존성을 완전히 없앨수 없기 때문에 추가적으로 아키텍쳐가 도입되어야 했다.  기존의 패턴은 수평관계의 의존성을 제거 한다면 아키텍처 도입으로 수직 관계의 의존성을 제거하였다. 물론 이러한 내용들을 받아들이는 사람들의 생각에 따라서 다르게 해석하고 다른 소스로 구현되어진다. 결국 큰 방향은 소드 코드의 의존성을 제거하는 쪽으로 흐른다고 볼수 있다.

위의 구조를 만들기 위해서 많은 라이브러리를 도입하게 되면 실제 학습의 시간이 가장 많이 소모 되므로 현 시점에서 가장 많이 쓰이는 라이브러리를 이용하여 구현 하였으며 실제 사용하고 있다.

Retrofit, OkHttp, RxAndroid, Retrolambda의 라이브러리를 이용하고 있다. 여기서 가장 어려운 학습은 역시 RxAndroid이다. 요즘에는 RxJava가 대세이다보니 많은 라이브러리들이 지원을 하고 있어서 적용시 큰 이슈는 없다. Dagger2를 사용하면 좋지만 아직 사용해본적이 없어서 관련 라이브러리는 배제하도록 하겠습니다.

이전글에 MVP 패턴에 대해서 간략적으로 설명하였으나 역시나 사용하다보니 불편한 점들은 업데이트가 되고 아키텍쳐와 합쳐지면서 더욱더 복잡해졌다.

Clean Architecture 적용의 목적은 순수 자바로 만들어서 의존성을 제거한다라고 생각하면 된다.

패키지 구조를 보도록 하겠습니다. 참조 사이트

domain :  모델을 제공 받는 방식에 대한 인터페이스입니다.

entity : 순수 모델입니다.

repository.local : 내부에서 모델을 제공. domain에 해당하는 인터페이스를 구현

repository.local.model : 내부모델

repository.remote : 외부에서 모델을 제공. domain에 해당하는 인터페이스를 구현

repository.remote.model : 외부모델

domain 샘플


public interface CommonInterface
{
Observable<CommonDateTime> getCommonDateTime();
}

repository.remote 샘플


public class CommonRemoteImpl implements CommonInterface
{
private Context mContext;

public CommonRemoteImpl(@NonNull Context context)
{
mContext = context;
}

@Override
Observable<CommonDateTime> getCommonDateTime()
{
return DailyMobileAPI.getInstance(mContext).getCommonDateTime().map((commonDateTimeDataBaseDto) -&gt;
{
CommonDateTime commonDateTime = null;

if (commonDateTimeDataBaseDto != null)
{
if (commonDateTimeDataBaseDto.msgCode == 100 &amp;&amp; commonDateTimeDataBaseDto.data != null)
{
commonDateTime = commonDateTimeDataBaseDto.data.getCommonDateTime();
} else
{
throw new BaseException(commonDateTimeDataBaseDto.msgCode, commonDateTimeDataBaseDto.msg);
}
} else
{
throw new BaseException(-1, null);
}

return commonDateTime;
}).observeOn(AndroidSchedulers.mainThread());
}
}

repository.remote.model 샘플


@JsonObject
public class CommonDateTimeData
{
@JsonField(name = "openDateTime")
public String openDateTime;

@JsonField(name = "closeDateTime")
public String closeDateTime;

@JsonField(name = "currentDateTime")
public String currentDateTime;

public CommonDateTimeData()
{
}

public CommonDateTime getCommonDateTime()
{
return new CommonDateTime(openDateTime, closeDateTime, currentDateTime);
}
}

entity 샘플


public class CommonDateTime
{
public String openDateTime;
public String closeDateTime;
public String currentDateTime;

public CommonDateTime()
{
}

public CommonDateTime(String openDateTime, String closeDateTime, String currentDateTime)
{
setDateTime(openDateTime, closeDateTime, currentDateTime);
}

public void setDateTime(String openDateTime, String closeDateTime, String currentDateTime)
{
this.openDateTime = openDateTime;
this.closeDateTime = closeDateTime;
this.currentDateTime = currentDateTime;
}
}

 

사용 방법


mCommonRemoteImpl.getCommonDateTime()//
.subscribe(commonDateTime ->;
{
onCommonDateTime(commonDateTime);
}, throwable -&gt;
{

}));

 

아래와 같은 구조로 되어있다.

스크린샷 2017-07-26 오후 4.09.36

domain에는 interface을 정의하여 데이터를 얻는 방식의 의존성을 버린다.

그리고 따로 로컬/네트워크 모델을 만들어서 저장소의 의존성을 버린다. 여기서 방식이란 예를 들어서 네트워크 / 로컬 데이터를 얻는 라이브러리를 어떠한 것 이용하는 경우이다. 결국 최종으로 얻는 CommonDateTime의 순수 모델은 각각의 단계에서 어떻게 얻어지는 몰라도 상관없다. 관련된 내용은 Clean Architecture의 의존성을 버리는 방식이다.

 

그렇다면 여기서 패턴을 추가 할수가 있다. 기본적을 MVP 패턴을 사용하도록 하겠다.

 

스크린샷 2017-07-26 오후 4.34.19

결국에는 Activity 조차도 의존성을 버리기 위해서 Interface를 두었다. 물론 Presenter, View도 Interface를 두어서 의존성을 없앴다. 계속적인 Interface로 의존성은 떨어지고 있지만 파일의 개수가 늘어나게 된다. 그래서 패키지를 나눌때 같은 속성 별로 패키지를 하지 않고 스크린 단위로 패키지를 하여 같은 스크린안에서 사용하는 파일들을 모아서 쉽게 접근할수 있도록 했다.

그리고 BasePresenter에서 Google Analytics 를 호출 할수 있도록 BaseAnalyticsInterface를 정의한다.

각각의 파일의 구조를 보도록 하겠다. 실제로는 더욱 복잡하지만 소스를 간추렸다.

BaseActivity는 BasePresenter와 연결되어서 Activity에 대한 의존성은 버린다. 대신 Activity는 Intent를 받는 역할로만 처리하였다.

BasePresenter는 BaseActivity, BaseInterface를 받아서 View와 의존성을 버리고, Analytics를 넣으므로써 OnBaseEventListener에서 발생하는 이벤트를 Google Analytics에 반영할수 있도록 했다.

BaseView는 OnBaseEventListener와 ViewDataBind를 받아서 Presenter와 의존성을 버리고 UI를 쉽게 접근할수 있도록 ViewDataBind로 처리하도록 했다. BaseView에서 activity변수를 받아서 약간 의아할수도 있다. 기존에는 Context를 보냈는데 사용하다 보니 View에서 어쩔수 없이 activity를 필요로 할때가 있는데 그래서 일반적으로 사용할때는 getConext()를 사용하여  Context를 넘겨주고 특별한 경우에만 activity를 이용하여 얻어갈수 있도록 따로 구현하였다. 물론 activity값을 View에서 직접 호출할수는 없다.

아직 부족한 점도 많고 새로이 뜨고 있는 MVVM 패턴과 구글에서 제공하는 AAC 라이브러리도 다시 적용해보아야 하기 때문에 계속적인 업데이트가 필요하지만 현시점에서 협업으로 사용하기에는 적절한 크기일것이라고 생각된다. 물론 접근하시는 분들의 입장에서는 다소 어렵다는 말씀이 있기도 하다. 추가적으로 Dagger2를 적용하는 부분도 고려해볼만하다고 생각됩니다.

 

이에 별도로 View의 xml이 점차 복잡해지기 때문에 해당 부분을 커스텀 뷰로 만들어서 재사용 목적으로 소스가 더 간략해질수 있다고 생각된다.

 

 

 

BaseActivity


public abstract class BaseActivity<T1 extends BasePresenter> extends AppCompatActivity
{
private BasePresenter mPresenter;

@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);

mPresenter = createInstancePresenter();

if (mPresenter.onIntent(getIntent()) == false)
{
finish();
return;
}

mPresenter.onPostCreate();
}

protected abstract
@NonNull
T1 createInstancePresenter();

public
@NonNull
T1 getPresenter()
{
if (mPresenter == null)
{
mPresenter = createInstancePresenter();
}

return (T1) mPresenter;
}

@Override
protected void onStart()
{
super.onStart();

if (mPresenter != null)
{
mPresenter.onStart();
}
}

@Override
protected void onResume()
{
super.onResume();

if (mPresenter != null)
{
mPresenter.onResume();
}
}

@Override
protected void onSaveInstanceState(Bundle outState)
{
if (mPresenter != null)
{
mPresenter.onSaveInstanceState(outState);
}

super.onSaveInstanceState(outState);
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState)
{
super.onRestoreInstanceState(savedInstanceState);

if (mPresenter != null)
{
mPresenter.onRestoreInstanceState(savedInstanceState);
}
}

@Override
protected void onPause()
{
super.onPause();

if (mPresenter != null)
{
mPresenter.onPause();
}
}

@Override
protected void onDestroy()
{
super.onDestroy();

if (mPresenter != null)
{
mPresenter.onDestroy();
}
}

@Override
public void onBackPressed()
{
if (mPresenter != null)
{
if (mPresenter.onBackPressed() == false)
{
super.onBackPressed();
}
} else
{
super.onBackPressed();
}
}

@Override
public void finish()
{
super.finish();

if (mPresenter != null)
{
mPresenter.onFinish();
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
super.onActivityResult(requestCode, resultCode, data);

if (mPresenter != null)
{
mPresenter.onActivityResult(requestCode, resultCode, data);
}
}
}

BaseActivityInterface


public interface BaseActivityInterface
{
boolean onIntent(Intent intent);

void onStart();

void onResume();

void onPause();

void onDestroy();

void onFinish();

boolean onBackPressed();

void onSaveInstanceState(Bundle outState);

void onRestoreInstanceState(Bundle savedInstanceState);

void onActivityResult(int requestCode, int resultCode, Intent data);
}

BasePresenter


public abstract class BasePresenter<T1 extends BaseActivity, T2 extends BaseViewInterface> implements BaseActivityInterface
{
private T1 mActivity;

private T2 mOnViewInterface;

public BasePresenter(@NonNull T1 activity)
{
mActivity = activity;

mLock = new DailyLock(activity);

mOnViewInterface = createInstanceViewInterface();

constructorInitialize(activity);
}

protected abstract
@NonNull
T2 createInstanceViewInterface();

public abstract void constructorInitialize(T1 activity);

public abstract void setAnalytics(BaseAnalyticsInterface analytics);

public abstract void onPostCreate();

public T1 getActivity()
{
return mActivity;
}

public void setContentView(@LayoutRes int layoutResID)public interface BaseActivityInterface
{
boolean onIntent(Intent intent);

void onStart();

void onResume();

void onPause();

void onDestroy();

void onFinish();

boolean onBackPressed();

void onSaveInstanceState(Bundle outState);

void onRestoreInstanceState(Bundle savedInstanceState);

void onActivityResult(int requestCode, int resultCode, Intent data);
}
{
if (mOnViewInterface == null)
{
throw new NullPointerException("mOnViewInterface is null");
} else
{
mOnViewInterface.setContentView(layoutResID);
}
}

public
@NonNull
T2 getViewInterface()
{
if (mOnViewInterface == null)
{
mOnViewInterface = createInstanceViewInterface();
}

return mOnViewInterface;
}

@Override
public void onStart()
{

}

@Override
public void onResume()
{

}

@Override
public void onPause()
{
}

@Override
public void onDestroy()
{
}

@Override
public void onFinish()
{

}

@Override
public boolean onBackPressed()
{
return false;
}

@Override
public void onSaveInstanceState(Bundle outState)
{

}

@Override
public void onRestoreInstanceState(Bundle savedInstanceState)
{

}

protected void finish()
{
mActivity.finish();
}
}

BaseViewInterface


public interface BaseViewInterface
{
void setContentView(int layoutResID);

void setContentView(int layoutResID, ViewGroup viewGroup);
}

BaseView


public abstract class BaseView<T1 extends OnBaseEventListener, T2 extends ViewDataBinding> implements BaseViewInterface
{
private BaseActivity mActivity;
private T2 mViewDataBinding;
private T1 mOnEventListener;

protected abstract void setContentView(T2 viewDataBinding);

public BaseView(BaseActivity activity, T1 listener)
{
if (activity == null || listener == null)
{
throw new NullPointerException();
}

mActivity = activity;
mOnEventListener = listener;
}

@Override
public final void setContentView(int layoutResID)
{
if (layoutResID != 0)
{
mViewDataBinding = DataBindingUtil.setContentView(mActivity, layoutResID);
}

setContentView(mViewDataBinding);
}

@Override
public final void setContentView(int layoutResID, ViewGroup viewGroup)
{
mViewDataBinding = DataBindingUtil.inflate(LayoutInflater.from(mActivity), layoutResID, viewGroup, false);

setContentView(mViewDataBinding);
}

protected void setVisibility(int visibility)
{
if (mViewDataBinding == null)
{
return;
}

mViewDataBinding.getRoot().setVisibility(visibility);
}

protected
@NonNull
Context getContext()
{
return mActivity;
}

protected T2 getViewDataBinding()
{
return mViewDataBinding;
}

protected
@NonNull
T1 getEventListener()
{
return mOnEventListener;
}

protected int getColor(int resId)
{
return mActivity.getResources().getColor(resId);
}

protected String getString(int resId)
{
return mActivity.getString(resId);
}
}

OnBaseEventListener


public interface OnBaseEventListener
{
void onBackClick();
}

BaseAnalyticsListener

public interface BaseAnalyticsInterface
{

}

 

 

Written by pinkredmobile

2017/07/26 at 4:59 pm

Android Custom View 만들때 merge태그

leave a comment »

Custom View를 만들때 일반적으로 ViewGroup을 상속 받아서 layer를 inflate시켜서 구현하는데 layer의 깊이를 줄이기 위해서 merge태그를 사용하다보니 merge 태그안의 UI가 제대로 정렬되지 않아서 수정시에 다시 merge태그를 없애고 다시 원래 ViewGroup 태그를 넣어서 GUI  수정후에 주석 처리하는 방식으로 진행했는데 매번 보는 사람이나 수정하는 사람이나 매우 귀찮았다고 할수 있다. 해결 방안이다.

Written by pinkredmobile

2017/07/26 at 10:47 am

프로그래밍(programming)에 게시됨

Tagged with ,

Android Rxjava2

leave a comment »

  • Rxjava2를 시작한지 얼마 되지는 않았다.
  • 관련된 서적도 사보고 해보았지만 밀려드는 신 기술에 이론만 파악될뿐 실무에 적용하려고 하니 너무나 어려운 문제가 많았다. 그중에서 가장 어려운 이슈는 답이라는 것이다.  1에서 10까지 더하는  코드를 작성하고자 한다면 한 최소 3가지 이상의 방법이 있을것이다. 대략 어떤 방식으로 하면 좋을지는 어렵지 않게 생각한다. 하지만 Rxjava에서 이런 경우 어떻게 해야할까 라고 생각할테 마땅히 답을 줄수 있는 사이트도 찾아보기 힘들고 국내 관련 블로그도 쉽지 않다.
  • 그래서 내가 쓰면서 아 이렇게 하면 되더라를 체험하면서 얻는 내용들을 정리하려고 한다.
  • Rxjava2를 사용하고 RxAndroid를 연동하여 사용한다.
  • CompositeDisposable를 사용하여 관리한다.
  • ramda를 사용한다.
  • 네트워크는 retrofit2를 사용한다.

계속적으로 사용하면서 추가하도록 한다.

  1. 스케줄러는 스레드로 결과는 메인 스레드로 해야 할 경우(예시로 네트워크 연동)

    mMobileService.getSuggests(url, keyword)//
                .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
  2. class A →  class B의 형식으로 변환 시키고 싶은 경우.

    BaseDto -> List 로 변환하여 리턴
    Observable getSuggests(String keyword);
    MobileAPI.getInstance(context).getSuggests(keyword).map((suggestsDataBaseDto) ->
    {
        List list = null;
        if (suggestsDataBaseDto != null)
        {
            if (suggestsDataBaseDto.code == 100 && suggestsDataBaseDto.data != null)
            {
                list = suggestsDataBaseDto.data.getSuggestList();
            } else
            {
                throw new BaseException(suggestsDataBaseDto.code, suggestsDataBaseDto.messasge);
            }
        } else
        {
            throw new BaseException(-1, null);
        }
        return list;
    }).observeOn(AndroidSchedulers.mainThread());
  3. 특정 시간 후에 스케줄러가 동작하는 경우

    1) 스케줄러를 호출하고 특정 시간후에 결과를 받는다.

    addCompositeDisposable(suggestRemote.getSuggests(keyword)//
        .delay(5000, TimeUnit.MILLISECONDS).subscribe(suggests -> onSuggests(suggests), throwable -> onSuggests(null)));

    2) 특정시간 후에 스케줄러를 호출하고 결과를 받는다.

    addCompositeDisposable(suggestRemote.getSuggests(keyword)//
        .delaySubscription(500, TimeUnit.MILLISECONDS).subscribe(suggests -> onSuggests(suggests), throwable -> onSuggests(null)));
  4. 두개의 스케줄러가 끝날때까지 대기하여 둘다 끝나면 실행하는 경우.
Observable.zip(transitionObservable, networkObservable, new BiFunction<Boolean, Boolean, Boolean>()
{
@Override
public Boolean apply(Boolean o, Boolean o2) throws Exception
{
return null;
}
}).subscribe(new Consumer<Boolean>()
{
@Override
public void accept(Boolean aBoolean) throws Exception
{
ExLog.d("pinkred : " + aBoolean);
}
});

5. 순차적으로 실행시키고 싶을때 예를 들어서 시간을 얻은 후에 얻은 시간 값으로 상세화면을 얻고 해당 값으로 기타등등을 얻고 싶을때..

getCommonDateTime().flatMap(new Function<CommonDateTime, Observable<User>>()
{
@Override
public Observable<User> apply(@io.reactivex.annotations.NonNull CommonDateTime commonDateTime) throws Exception
{
return new ProfileRemoteImpl(getActivity()).getProfile();
}
}).flatMap(new Function<User, Observable<UserInformation>>()
{
@Override
public Observable<UserInformation> apply(@io.reactivex.annotations.NonNull User user) throws Exception
{
return new ProfileRemoteImpl(getActivity()).getUserInformation();
}
}).subscribe(new Consumer<UserInformation>()
{
@Override
public void accept(@io.reactivex.annotations.NonNull UserInformation userInformation) throws Exception
{

}
}));

6. RxJava로 뒤로가기 버튼 확인 기능 구현하기

Written by pinkredmobile

2017/05/04 at 9:11 am

프로그래밍(programming)에 게시됨

Tagged with ,

Android build gradle

leave a comment »

build gradle 환경을 수정해보는 것은 어쩌면 또 다른 경험일수 있다고 생각합니다.

실제 build에 문제가 있지 않지만 조금더 편리하게 라는 단어에 노력해 보기로 했습니다.

목표

  1. debug, release 버전의 서버 구분
  2. 빌드 파일 이름 바꾸기
  3. git 연동
  4. productFlavor을 통한 버전 나누기
  1. debug, release 버전의 서버 구분은 여러가지 방법이 있지만 파일을 나누는 방식을 선택했다. release 버전의 서버는 한개인데 debug 서버가 여러가지 일수 있어서 개발중에 debug서버를 바꾸는 경우가 생겨서 이다. 방법은 간단하다.  아래와 같이 debug, release  폴더를 만들어 주고 패키지 구조를 똑같이 만들어서 서버 구성 파일을 따로따로 넣어주면 된다.

%e1%84%89%e1%85%b3%e1%84%8f%e1%85%b3%e1%84%85%e1%85%b5%e1%86%ab%e1%84%89%e1%85%a3%e1%86%ba-2017-02-23-%e1%84%8b%e1%85%a9%e1%84%8c%e1%85%a5%e1%86%ab-8-50-22

2. 빌드 파일 이름 바꾸기

이름을 바꾸는 방법은 다양한데 회사 마다 특유의 형식이 있을수 있기 때문에 형식에 간단하게 소개하겠습니다.


android {
....

applicationVariants.all { variant ->
// 날짜 넣기
def simpleDateFormat = new SimpleDateFormat("yyMMdd");
simpleDateFormat.setTimeZone(TimeZone.getDefault());
// debug, release 빌드 구분
if (variant.buildType.name == "release") {

} else {

}

// 버전 넣기
apkName += "-v" + defaultConfig.versionName;
apkName += "-" + simpleDateFormat.format(new Date());
apkName += ".apk";

variant.outputs.each { output ->
output.outputFile = new File(output.outputFile.parent, apkName);
}
}
}

3. git 연동

git 연동은 의외로 어렵지 않다. git 명령어 그대로 넣으면 된다. 예를 들어서 git의 이름을 얻어오고 싶다면

<pre>def getGitName = { ->
    def stdout = new ByteArrayOutputStream()
    exec {
        commandLine 'git', 'config', 'user.name'
        standardOutput = stdout
    }

    return stdout.toString().trim();
}

def gitName = getGitName();</pre>

4. productFlavor을 통한 버전 나누기는 릴리즈시에 어떤 버전을 만들게 될지 결정 할수가 있다.

안드로이드 스튜디오에서 generate signed apk을 하면 아래와 같은 절차에서 Flavor을 선택할수 있다. 자세한 내용은 구글 사이트를 참조하세요.

%e1%84%89%e1%85%b3%e1%84%8f%e1%85%b3%e1%84%85%e1%85%b5%e1%86%ab%e1%84%89%e1%85%a3%e1%86%ba-2017-02-23-%e1%84%8b%e1%85%a9%e1%84%8c%e1%85%a5%e1%86%ab-9-03-53

gradle에서는 아래와 같은 방식으로 빌드되는 Flavor의 이름을 얻어올수 있다.

<pre>applicationVariants.all { variant ->
    ...

    def currentFlavor = variant.flavorName;

    ...
    variant.outputs.each { output ->
        output.outputFile = new File(output.outputFile.parent, apkName);
    }
}</pre>

 

Written by pinkredmobile

2017/02/23 at 9:09 am

Android Jack Compiler

leave a comment »

전혀 알지 못한 컴파일러 였다. 이 컴파일러를 아는데 도움을 준 realm강의 고마움을 표시합니다.

들어봤으면 해보고 싶은 마음이 굴뚝 같아서 진행해보기로하였다. 구글 가이드 문서

1.일단 가장 간단하게 Jack Compiler를 Gradle에 넣는다. (참고 : http://tools.android.com/tech-docs/jackandjill)

android {
...
buildToolsVersion ‘25.0.2’
defaultConfig {
// Enable the experimental Jack build tools.
jackOptions {
enabled true
}
}
...
}

여기서 만일 java 1.8버전의 문법 약간이나마 사용하고 싶은 경우에는

android {
...
buildToolsVersion ‘21.1.2’
defaultConfig {
// Enable the experimental Jack build tools.
jackOptions {
enabled true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

...
}

2. 이후 컴파일 후에 APT에 이슈가 발생하였다. 몰랐는데 APT가 2.2.0이후 버전인가에서는 APT가 통합되어 있어서 APT 로 시작하는 ‘com.neenbedankt.android-apt’ 여러가지 라이브러리를 삭제하고 다음과 같이 넣어주면 된다.(참조 : https://github.com/nickwph/annotation-processor-with-java8-jack-and-jill-android)

dependencies {
 // dagger 2
 compile 'com.google.dagger:dagger:2.5'
 annotationProcessor 'com.google.dagger:dagger-compiler:2.5'
 // auto-value
 compile 'com.google.auto.value:auto-value:1.2'
 annotationProcessor 'com.google.auto.value:auto-value:1.2'
 // butterknife
 compile 'com.jakewharton:butterknife:8.1.0'
 annotationProcessor 'com.jakewharton:butterknife-compiler:8.1.0'
 // logan square
 compile 'com.bluelinelabs:logansquare:1.3.6'
 annotationProcessor 'com.bluelinelabs:logansquare-compiler:1.3.6'
}

3. 기존에 오래된 라이브러리가 지원을 하지 않는 경우가 발생 하였다. Google Conversion Tracking and Remarketing for Android의 라이브러리가 지원하지 않아 문제였는다. 다행이 사용하지 않는 라이브러리여서  과감히 삭제후 재 빌드.

4. 거의다 왔다. 컴파일을 시작해보자. 빌드 시간이 대략 15분 걸렸다. ㅜㅜ 생각보다 너무 오래 걸려서 놀랐다 2번 연속으로 하면 out of memory 가 발생하기도 한다. 충분한 android studio 에 메모리를 잡아주었지만 발생하였다.

5. 결과는 기존 릴리즈 버전은 8.8메가 정도의 APK 크기이고 Jack Compiler을 이용해서 릴리즈 버전을 생성시키면 8.4메가 정도의 APK 가 생성된다. 고생 무지 했는데 0.4메가 줄임. ㅜㅜ

6. 여기서 이슈발생 Android 5.0이하에서는 MultiDex가 제대로 동작하지 않아서 실행시 “java.lang.NoClassDefFoundError” 가 발생하여 앱이 종료된다.

7. https://code.google.com/p/android/issues/detail?id=213483 나온데로 진행해 보았지만 아직 안됨. 추후에 MultiDex 이슈가 해결되면 다시 진행해 볼 예정입니다.

Written by pinkredmobile

2017/01/19 at 5:50 pm

[Android] Facebook – Keyframes

leave a comment »

페이스북의 ‘좋아요’ 를 누르고 있으면  이모티콘들이 열심히 움직이고 있다. 이를 구현하자고 하는 이슈가 있어서 찾아본 결과.

https://code.facebook.com/posts/354469174916519/keyframes-delivering-scalable-high-quality-animations-to-mobile-clients/

멋지게 페이스북에서 오픈소스로 제공하고 있다. 감탄..

  1. github에서 제공하는 After Effects Plugin 을 설치한다.
  2. After Effects를 이용하여 애니메이션을 만든다.(제약 사항있음)
  3. Plugin을 사용하여 데이터를 JSON 포맷으로 변형하여 받는다.
  4. 데이터를 Keyframes 라이브러리를 이용하여 호출하면 애니메이션이 됩니다.

Android, iOS, JS를 지원합니다. 처음 접했을때는 버전이 낮았는데 지금은 1.0 버전으로 정식 릴리즈 되었습니다. 물론 내부적으로는 현재 지원되지 않는 기능들을 몇가지 필요로 하여 추가적으로 코드를 수정하여 지원되도록 했습니다. 추가 하기도 어렵지 않고 소스코드도 복잡하지 않아서 지원하지 않는 기능들을 쉽게 추가할 수 있습니다.

처음에는 많은 고민이 있었는데 이렇게 쉽게 라이브러리로 제공되니 Facebook 너무 고맙네요.

Written by pinkredmobile

2016/12/29 at 11:44 am

[Android] Volley -> Retrofit2

leave a comment »

계속계속 시도해 보면서 문제점과 이슈들이 계속 발생해서 미루고 있었는데 드디어 바꿀수 있는 기회가 와서 바꾸어 보았다.

기존 구조는 OkHttp3 + Volley + JSON 구조로 되어 있어 특별히 문제가 되지는 않았지만 이제는 Volley를 놔주고 싶은 마음은 굴뚝 같았다. 특히나 네트워크 아파치 라이브러리를 더 이상 지원하지는 안드로이드 이슈와 더이상 업데이트가 되지 않는 Volley를 계속 가져갈수는 없었다. 하지만 구관이 명관인것은 확실했다.

현재 구조상의 가장 큰 이슈는 Retrofit2에서는 공식적으로 JSON 파서를 지원하지 않았다. GSON등등 여러가지 파서는 지원했지만 정작 기본적인 JSON 파서를 지원하지 않아서 다른 파서를 사용하여 POJO방식으로 변경시 너무나 많은 코드 수정으로 감당할 수가 없었습니다.

여러가지 방법으로 접근해 보려는 시도는 해보았으나..

Volley + GSON으로 시작하여 진행하는 방식 기존의 JSON 을 GSON Json으로 대체하여 비교적 비슷하게 가려고 했으나 JSON에 데이터가  null 인 경우 예외처리가 추가 되어야 하는 이슈가 있었는데 기존 JSON에서는 그냥 null값을 받았는데 이러한 부분에서 에러가 발생하여 일일이 null체크를 해주어야 하다보니 일이 너무 많아져서 포기했다.

그러다가 Retrofit2에서 JSON을 지원하는 커스텀 Converter를 찾게 되어서 다시금 희망이 생기고 Volley를 걷어내고 Retrofit2로 교채를 진행하기로 하였다.

대부분 같은 생각이 있는지 비슷한 과정을 진행한 회사의 블로그를 찾아서 도움을 받았다.

기존의 Volley 의 Queue에서 Cancel을 하는 부분이 가장 큰 이슈였는데 Retrofit2에서는 Cancel을 하기 위해서는 Call을 따로 저장해서 구현해야하는 불편한 이슈가 있었다.

처음에는 Annotation Tag를 넣어서 저장하려고 하였으나 현재 앱에서 사용되는 cancel방식이 서로  다르게 동작되어서 Tag를 직접 Class에서 직접 넣는 방식으로 수정하였다.

 

ExecutorCallbackCall executorCallbackCall = (ExecutorCallbackCall) mMobileService.requestServer(URL);
executorCallbackCall.setTag(tag);
executorCallbackCall.enqueue(callback);

 

어떻게든 내부적으로 쉽게 하려고 했으나 방법 못찾음. ㅜㅜ 기존의 Volley와 비슷한 방식으로 처리되어서 쉽기는 하나 아쉽기도 합니다. 추후 된되다면  Annotation에 Path를 변수로 넣듯이 Tag도 그렇게 할수 있으면 추후에 관리가 조금더 쉽게 될수 있지 않으려나 생각됩니다.

 

Written by pinkredmobile

2016/12/19 at 10:46 am

[Android] Retrofit2, Volley Test

leave a comment »

요즘에 Volley에서 아파치 라이브러리 이슈로 에러가 리포팅 되어서 요즘 인기가 많다는 Retrofit을 써보려고 했으나 관문이 너무 많아서 일단 속도 테스트를 해보았다.

  1. Retrofit를 적용하려면 기존의 JSONObject 방식에서 POJO방식으로 변환하기에는 너무나 많은 시간이 걸리기에 대체 방안을 생각하였다.
  2. GSON의 JsonParser를 이용하여 기존의 JSONObject와 비슷하여 POJO 방식 변경하는 것보다는 시간이 덜 걸릴것 같았다.
  3. Volley + GSON(JsonParser) 을 전부 변화하고 나니 이슈가 발생하였다. value=null인것들을 기존에는 JSONObject가 알아서 null을 넣어주었나 GSON에서는 일일이 null체크를 해주어야 하니 기존에 쌓아둔 업적이 너무 많아서 고민을 하게 되었다.
  4. 그러던 와중 속도 테스트라도 해보기로 했다. 1차 적으로 Volley + GSON(JsonParser) 과 Volley + JSONObject의 속도를 측정해보기로 했다.  파싱 속도만 측정했다. Volley를 사용하기 때문에 받은 데이터를 String로 변환시키고 나서 파서가 동작된다.
리스트 파싱 속도(ms)
GSON JsonParser
JSONObject
108
86
87
86
76
51
52
58
47
70

5. 속도 비교를 하고 나서 다시금 바꾸고 싶지 않았지만 실제 모든 방식으로 테스트 해봐야 하기 때문에 아래와 같이 테스트해보았다. 아래 테스트는 리스트를 요청하는 네트워크 시간과 파싱하는 시간을 같이 체크했다. 시간이 많이 나오는 경우는 캐쉬가 동작하지 않는 경우이다. 실제 데이터를 보면 Volley를 이용할때보다 Retrofit을 이용할때 속도가 더 빨랐으며 POJO 방식을 사용할때 조금더 빨랐다.

리스트
Retrofit + GSON(POJO)
Volley + GSON(JsonParser)
Volley + JSON
Volley + GSON(POJO)
Retrofit + GSON(JsonParser)
request → response 후 파싱까지만 4630
210
192
4526
310
213
188
4491
4675
344
402
406
4593
410
457
389
4661
342
4660
381
367
361
4630

375

425
482
4736
422
396
365
455
4661
4897
241
204
312
252
247
4575
273

Written by pinkredmobile

2016/11/21 at 6:12 pm