Google Play Service Lib 사이즈가 어느 순간부터 MB 단위로 커졌습니다.

라이브러리를 추가해서 빌드하면 APK 사이즈가 몇 메가가 추가되는게 불만이었는데,


버전 6.5부터 필요한 API 군을 선별해서 빌드할 수 있게 되었고,

Android Studio에서 build.gradle 만 살짝 변경해주면 APK사이즈를 1.2MB나 줄일 수 있었습니다.


모듈 디렉토리 하위에 있는 build.gradle 파일을 보면,

보통은 아래처럼 기록했었습니다.

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:21.0.3'
    compile 'com.google.android.gms:play-services:7.5.0'
}


이제는 필요한 API 군만 선택해서 기록할 수 있습니다.


만약, 지도와 위치서비스를 사용한다면...

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:21.0.3'
    compile 'com.google.android.gms:play-services-location:7.5.0'
    compile 'com.google.android.gms:play-services-maps:7.5.0'
}


Hello world 프로젝트를 만들어서 APK사이즈를 비교해봤습니다.


 LIB 없음

LIB전체 추가 

선별추가(Map, Location) 

 886KB

3,134KB 

1,841KB 



이제껏 몰랐네요. >.<

이클립스에서 스튜디오로 옮기려고 이것저것 둘러보는 중에 알게되었습니다.


역시 개발자가 멍청하면 사용자의 스마트폰 패킷을 남용하게 되네요.

다음 배포부터 적용해야겠습니다.



[참고]

https://developers.google.com/android/guides/setup#add_google_play_services_to_your_project

Posted by 데브로망스

댓글을 달아 주세요

최근에는 구식인 UI흐름이지만,

로그아웃을 했을 때, 로그인 화면을 띄워야 할 때가 있다.

이때, 스택에 쌓여있는 기존 Activity들을 모두 제거할 수 있다면 흐름에 방해되는 골치아픈 문제들을 한 방에 해결할 수 있다.


Intent intent = new Intent(context, classToShow);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) {
	intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
}
else {
	intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
}
startActivity(intent);

간단히 플래그에 대해 설명하면, 

FLAG_ACTIVITY_CLEAR_TASK : 현재 TASK를 비운다.

FLAG_ACTIVITY_NEW_TASK : 새로운 TASK를 생성한다. (Activity가 아닌 곳에서 Activity를 띄울 때, 종종 사용하는 플래그)



한 가지 문제는 첫 번째 플래그, FLAG_ACTIVITY_CLEAR_TASK 는 API Level 11(허니콤)부터 사용이 가능해서 진저브레드를 지원하는 앱에서는 사용이 불가능하다. (당연하지만 플래그를 적용한다해도 원하는데로 동작하지 않는다.)

만약, 진저브레드를 지원해야한다면 버전코드를 확인해서 동작을 달리해야한다.

진저브레드는 FLAG_ACTIVITY_CLEAR_TOP를 사용해서 처리했는데,

애초에 진저브레드의 경우에는 로그인화면에서 로그인이 완료되었다고 해도 Activity를 종료하지 않았다.

반면에, 허니콤 이상의 경우 쿨하게 finish()를 호출했다. 아래 코드 처럼.


if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) {
	finish();
}


즉, 진저브레드는 FLAG_ACTIVITY_CLEAR_TOP을 이용해서, 스택 맨 하단에 있는 로그인 Activity를 남기고 중간의 Activity는 종료시키는 방법을 적용. 상위 OS 버전과 동작을 맞췄다.


이 경우, 또 다른 문제가 있는데, 진저브레드에서는 앱을 종료시킬 때 단순히 메인 Activity를 finish()를 호출하면 밑에 있었던 로그인 Activity가 보이게된다. 결국에는 (권장하는 방법은 아니라고 하지만...) killProcess를 사용하면 된다.


if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) {
	finish();
}
else {
	android.os.Process.killProcess(android.os.Process.myPid());
}



지금까지 찾은 가장 깔끔한 방법인데, 더 우아한  방법이 있다면 조언바랍니다.

사실은 최근 트랜드에 맞는 UI흐름을 구성하면 좋겠지만

살다보면 참 마음대로 안 될때가 많네요. ^0^




Posted by 데브로망스

댓글을 달아 주세요

남들이 만든 리소스를 훔쳐보고 싶을 때가 있네요.

이것 저것 찾다가 가장 깔끔한 방법이 있어서 정리해 놓습니다.


apktool 이네요.


JAR파일은 여기서 다운로드 합니다.


윈도우즈 커멘트 창에서 apttool jar를 실행시키면 되는데, 방법은 아래와 같습니다.


>java -jar apktool_2.0.0b9.jar decode --no-src -f DOWNLOADED.apk


여러 옵션들이 있는데, 여기서 사용한 옵션이면 충분하네요.


"--no-src" : JAVA코드는 건너뛰기

"-f" : 기존에 있는 폴더는 강제로 삭제하고 다시 디컴파일하기


더 많은 옵션들은 여기를 보시면 됩니다.


오류가 발생할 때는 새 버전을 다운로드 받으면 되더군요.


^^

Posted by 데브로망스

댓글을 달아 주세요


[문제]
InentService를 상속받아서 구현을 했는데, onHandleIntent()가 호출되지 않는 경우가 발생했습니다.

[해결방법]
구글 문서에 따르면, Intent Service를 구현할 때는 생성자와 onHandleIntent()만 구현하면 된다라고 설명하고 있습니다. 다른 콜백 함수들은 구현해도 되고 안해도 되는 것 처럼 적어놨습니다.

That's all you need: a constructor and an implementation of onHandleIntent().

그런데, (저의 경우) onHandleIntent()가 호출되지 않아서 방법을 찾아봤더니, onStartCommand()함수를 구현하면 해결이 되었습니다. (그렇다면 항상 onStartCommand()를 재정의해야 하는 것 아닌가 싶은데 왜 저렇게 적어놨는지는 모르겠습니다.) 

이때, 반드시, 부모 함수(super)를 호출해줘야 하고 반환 값은 반드시 default로 구현된 내용을 유지해줘야 합니다. (즉, 반환값을 임의로 "START_STICKY"를 반환하면 안됩니다.)

If you decide to also override other callback methods, such as onCreate(), onStartCommand(), or onDestroy(), be sure to call the super implementation, so that the IntentService can properly handle the life of the worker thread.

For example, onStartCommand() must return the default implementation (which is how the intent gets delivered to onHandleIntent()):

결과적으로 코드는 아래와 같은 형태가 됩니다. 이제 다른 Activity나 다른 App에서 startService()를 호출하면 onHandleIntent()가 잘 호출됩니다. 당연히 startService()를 호출할 때 넣어준 Intent도 파라메터로 잘 들어옵니다.

public class MyIntentService extends IntentService {
	public static final String PARAM_IN_MSG = "IN_MSG";

	public MyIntentService() {
		super("MyIntentService");
	}
	
	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		return super.onStartCommand(intent, flags, startId);
	}
	
	@Override
	protected void onHandleIntent(Intent intent) {
		String msg = intent.getStringExtra(PARAM_IN_MSG);
		String resultTxt = msg + " "+ DateFormat.format("MM/dd/yy h:mmaa", System.currentTimeMillis());
		Toast.makeText(this, "onHandleIntent Called : " + resultTxt, Toast.LENGTH_LONG).show();
	}
}




[원문보기]


[추가1]
아무래도 뭔가 이상해서, IntentService의 코드를 찾아봤습니다.
코드에서는 onStartCommand()에서 onStart()를 호출. onStart()에서는 핸들러에 메시지를 던지고 결국 onHandleIntent()를 호출하고 있습니다. 코드에는 문제가 없어보이는데, 왜 상속받아 만든 클래스에서 onStartCommand()를 재정의해야만 onHandleIntent()가 호출되는 걸까요? 혹시, 아시는 분 계시면 한 수 가르쳐주세요 ^^

private final class ServiceHandler extends Handler {
	public ServiceHandler(Looper looper) {
		super(looper);
	}
	@Override
	public void handleMessage(Message msg) {
		onHandleIntent((Intent) msg.obj);
	}
}
@Override
public void onStart(Intent intent, int startId) {
	Message msg = mServiceHandler.obtainMessage();
	msg.arg1 = startId;
	msg.obj = intent;
	mServiceHandler.sendMessage(msg);
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
	onStart(intent, startId);
	return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}







Posted by 데브로망스

댓글을 달아 주세요

  1. luke 2016.08.19 14:02  댓글주소  수정/삭제  댓글쓰기

    AndroidManifest.xml 에 service 로 추가 하셨는지 궁금하네요. 혹시나 해서

화면꺼짐 상태에서 GCM 메시지 수신 안되는 문제



갑자기 잘 되던 GCM 메시지 수신이 화면이 잠긴(꺼진, Screen Off)상태에서 수신이 안되는 현상이 발생했다.

화면을 켜면 그때 바로 수신이 된다. 정확히 말하면 화면을 켜고 잠금화면을 풀면 그때 바로 수신이 된다.

또한, 다른 스마트폰에서는 잘되기도 한다. 이유가 뭘까?


그 이유는 GCM을 구글쪽에 발송하는 중계 서버에 문제였는데 GCM을 구글 GCM서버로 발송할 때 "delay_while_idle" 이라는 옵션이 있다. 이 옵션의 Default 값이 안드로이드4.0 이전에는 false였다가 4.0부터 true로 바뀌었기 때문이다. (문제가 발생한 단말기의 버전은 4.1.2 입니다.)


관련해서 안드로이드 공식페이지에서 내용을 찾아봤다.


If the device is connected but idle, the message will still be delivered right away unless the delay_while_idle flag is set to true. Otherwise, it will be stored in the GCM servers until the device is awake.  


dealy_while_idle 옵션은 "true" 또는 "false" 값에 따라 수신되는 동작을 변경할 수 있는데, 만약, false로 설정하면 화면이 꺼진 후 스마트폰이 idle상태가 된 상태에서도 GCM은 바로 수신이 되지만, true로 설정하면 GCM 서버에 저장되었다가 스마트폰이 켜질 때(awake) 수신이 된다.


조금 더 내용을 살펴보면...


If the device is not connected to GCM, the message will be stored until a connection is established (again respecting the collapse key rules). When a connection is established, GCM will deliver all pending messages to the device, regardless of the delay_while_idle flag. If the device never gets connected again (for instance, if it was factory reset), the message will eventually time out and be discarded from GCM storage. The default timeout is 4 weeks, unless the time_to_live flag is set.


스마트폰이 GCM서버에 연결되어 있지 않으면 메시지는 연결이 될때까지 저장되고, 이후에 연결이 완료되면 GCM서버는 그때 미루고 있었던 메시지를 스마트폰으로 전달하게 된다. 중요한 것은 이 과정에서 dealy_while_idle 옵션이 어떻게 되어 있는지 상관없이 동작을 한단다. 스크린을 끈 상태에서 테스트했을 때, 계속 수신이 안되다가도 한 번씩 수신이 되기도 했는데 OS 내부에서 주기적으로 GCM서버에 연결을 하는 동작이 있는 것으로 추측만 했는데 바로 이 내용인가 싶다. 이건 그저 추측이다. (혹시 아시는 분 계시면 명쾌한 답 부탁드립니다. ^^) 


만약, 스마트폰이 공장초기화 등으로 인해 GCM 서버에 더이상 연결을 하지 않으면 4주간 유지했다가 이후에 "time out"이 되어 소멸됩니다. 이 time out 시간은 "time_to_live"옵션으로 변경이 가능하다. (테스트까지는 안했음)


그런데, delay_while_idle이란 옵션이 왜 존재하고 왜 default값을 false에서 true로 변경을 한걸까?


GCM will usually deliver messages immediately after they are sent. However, this might not always be possible. For example, the device could be turned off, offline, or otherwise unavailable. In other cases, the sender itself might request that messages not be delivered until the device becomes active by using the delay_while_idle flag. Finally, GCM might intentionally delay messages to prevent an application from consuming excessive resources and negatively impacting battery life.


이 내용으로 짐작해보면, 결국 사용자가 스마트폰을 사용하지 않을 때 GCM을 수신받아서 뭔가 작업을 하는 것은 결과적으로 더 많은 리소스와 배터리를 소비하게되는데 이를 방지하기 위해 제공하는 옵션이다. 결국, 사용자가 스마트폰을 보고있지 않은 상황에서 꼭 하지 않아도 되는 기능이라면 잠시 미뤄두라는 거다.


결론

GCM을 전송할 때는 중요한 기능은 delay_while_idle 옵션을 false로, 그렇지 않다면 true로 전송을 하자. 또한, OS별로 Default 값이 다를 수 있으니, GCM 중계 서버(구글꺼 말고 우리꺼)에서 별도의 default 값을 반드시 정하고 서비스하는 것이 맞다.




[참고문서]




Posted by 데브로망스

댓글을 달아 주세요

  1. 2015.07.28 17:56  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

프레그먼트 다루기 (Managing Fragments)


액티비티에서 프레그먼트를 다루기 위해서는  FragmentManager를 이용해야한다. FragmentManager의 객체를 얻으려면 액티비티에서 제공하는 getFragmentManager()를 호출하면 된다.


FragmentManager에서 제공하는 기능으로 할 수 있는 것을 나열해 보면...


  • findFragmentById() 또는 findFragmentByTag() 함수로 액티비티가 갖고 있는 프레그먼트들에 접근할 수 있다. UI를 갖는 프레그먼트의 경우 findFragmentById(), findFragmentByTag() 둘 중 하나를 사용하면되는데, UI가 없는 프레그먼트의 경우에는 findFragmentByTag()함수만을 사용할 수 있다.
  • popBackStack() 함수를 사용해서 백스택으로부터 프레그먼트를 꺼낼 수 있다. (사용자가 Back 명령을 했을때)
  • 백스택의 변경 사항을 처리하기 위한 리스너는 addOnBackStackChangedListener()함수로 등록할 수 있다.


이러한 함수들의  좀 더 자세한 내용은 FragmentManager 클래스에 대한 문서를 참고하자.


이전 글에서 거론한대로 우리는 FragmentManager 클래스를 통해 프레그먼트 트랜잭션(추가, 삭제 등)을 하는데 필요한 FragmentTransaction 객체를 생성할 수 있다.




프레그먼트 트랜잭션 수행하기 (Performing Fragment Transactions)


액티비티에서 프레그먼트를 사용하는 것의 가장 큰 특징은 사용자의 동작에 따라 추가, 삭제, 교체 그리고 그 외 다른 동작들을 수행할 수 있다는 것이다. 액티비티에 적용한 각각의 변경 셋트를 Transaction(이하 트랜잭션)이라 부르는데, FragmentTransaction 클래스가 제공하는 API를 사용해서 이러한 트랜잭션을 수행할 수 있다. 또한, 각각의 트랜잭션에 대한 이력을 액티비티가 관리하는 백스택에 저장한 후 프레그먼트를 조작해서 사용자에게 뒤로가기 기능을 제공할 수 있다.


FragmentTransaction의 인스턴스는 FragmentManager 클래스로부터 가져올 수 있다.


FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();


각각의 트랜잭션은 우리가 한 번에 수행하고자하는 변경 내용의 셋트라고 할 수 있다. 우리에게 주어진 하나의 트랜잭션 동안 add(), remove(), replace()와 같은 함수를 사용해서 원하는 변경 내용을 모두 모아 셋트를 구성할 수 있다. 그 다음에 반드시 commit() 함수를 호출해서 실제 액티비티에 이 트랜잭션을 반영하면 된다.


이번 변경사항을 백스택에 추가하기 위해 commit() 함수를 호출하기 전에 addToBackStack()을 호출할 수도 있다. 백스택은 액티비티가 관리하는데, 백버튼을 눌렀을 때 사용자에게 이전 프레그먼트 상태로 되돌려줄 수 있도록 해준다.


예를 들어 아래 코드는 하나의 프레그먼트를 다른 녀석으로 교체해주면서 백스택에 이전 상태를 저장하고 있다.


// Create new fragment and transaction
Fragment newFragment = new ExampleFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();

// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction
.replace(R.id.fragment_container, newFragment);
transaction
.addToBackStack(null);

// Commit the transaction
transaction
.commit();


이 코드에서 newFragment는 R.id.fragment_container ID로 식별되는 레이아웃 컨테이너에 포함된 다른 프레그먼트를 대체하게 된다. addToBackStack()함수를 호출해서 교체하는 트랜잭션은 백스택에 저장되고 사용자는 이 트랜잭션을 되돌릴 수 있게 되어 결과적으로 백버튼을 눌러 이전 프레그먼트로 돌아올 수 있다.


만약 우리가 add(), remove()와 같은 여러 변경 사항을 한 트랜잭션에 추가하고 addToBackStack()을 호출한다면, commit()을 호출하기 전의 모든 변경 내용은 개별 트랜잭션 단위로 백스택에 추가되고 백버튼은 그 모든 변경 내용을 되돌려줄 것이다.


FragmentTransaction 클래스에 변경 내용을 추가할때는 호출 순서가 크게 상관없지만, 아래 내용은 주의해야한다.

  • commit()함수는 맨 마지막에 호출해야한다.
  • 동일한 컨테이너에 다수의 프레그먼트를 추가할 때, 그 추가하는 순서는 뷰 계층에서 화면에 보여지는 순서를 결정한다.


프레그먼트를 삭제하는 트랜잭션을 수행할 때, addToBackStack()을 호출하지 않으면 해당 프레그먼트는 트랜잭션이 최종반영(commit)될 때 완전 제거(destroy)되어 사용자는 그 프레그먼트로 돌아갈 수 없게된다. 반면, addToBackStack()함수를 호출했을 때는 프레그먼트는 잠시 정지(stop)되고 사용자가 이전으로 되돌아오려고 할 때 재개(resume)된다.


주의 : 각각의 프레그먼트 트랜잭션이 수행되는 동안, 우리는 최종반영(commit) 이전에 setTransitiojn()을 호출해서 애니메이션을 적용할 수 있다.



commit()을 호출한다고 해서 트랜잭션이 바로 실행되지 않고 액티비티의 UI 쓰레드(메인쓰레드)의 스케쥴에 따르게되고 이 쓰레드가 수행 가능할 때에 맞춰서 실행된다. 하지만, 우리는 executePendingTransactions()을 호출해서 UI쓰레드가 commit()호출되는 즉시 트랜잭션이 실행되도록 할 수 있다. 하지만, 이렇게 하는 것은 해당 트랜잭션이 다른 쓰레드와 의존적이지 않은 경우라면 딱히 필요하지 않다.


주의 : 우리는 액티비티가 현재 상태를 저장하기 전(사용자가 액티비티를 떠날 때)에만 commit()함수를 사용할 수 있다. 이 시점 이후에 commit을 시도하면 예외가 발생한다. 이렇게 한 이유는 액티비티가 원복될 때 commit 이전의 상태가 저장되지 않을 수 있기 때문이다. commit에 대한 내용이 날라가도 괜찮은 상황이라면 commitAllowingStateLoss()함수를 사용하면 된다.




[원문보기]



Posted by 데브로망스

댓글을 달아 주세요

프레그먼트 만들기(Creating a Fragment)



프레그먼트를 만들기 위해서는 반드시 Fragment 클래스를 상속받아야 한다. (또는 이미 생성한 서브클래스를 상속받아도 됨) 프레그먼트 클래스는 액티비티와 매우 유사한 코드로 작성되어있는데, onCreate(), onStart(), onPause(), onStop()과 같은 액티비티에서 제공하던 유사한 콜백함수를 제공한다. 사실, 기존에 개발했던 앱을 프레그먼트를 이용하도록 변경한다면 액비티비 콜백 함수들에서 구현했던 코드를 각각 프레그먼트의 콜백함수로 그대로 옮겨와도 된다.


일반적으로 우리는 최소한 아래 나열된 생명주기(lifecycle) 함수를 구현해야한다.


onCreate()

프레그먼트가 생성될 때, 시스템이 호출한다. 구현 시, 프레그먼트가 pause 또는 stop 되었다가 다시 resume이 되는 상황에 영향받지 않도록 유지해야하는 필수적인 콤포넌트를 초기화해야 한다.


onCreateView()

프레그먼트가 처음으로 UI를 그리는 시점에 시스템이 호출한다. 프레그먼트에 UI를 넣으려면 프레그먼트 레이아웃의 루트격인 이 함수에서 VIew를 리턴해야한다. 만약 UI가 필요없는 프레그먼트인 경우 null을 반환하면 된다. 


onPause()

사용자가 프레그먼트를 떠나는 순간 시스템에서 호출한다. (프레그먼트가 반드시 destroy되는 상황은 아니다) 이 함수는 보통 해당 사용자의 세션에서 유지되어야 하는 모든 변경 사항을 저장하는 곳이다. (당장에는 사용자가 해당 프레그먼트로 돌아오지 않을 수 있기 때문이다)


대부분의 앱은 프레그먼트를 작성할 때 적어도 위의 3개 함수를 구현해야 한다. 하지만, 프레그먼트의 생명주기의 다양한 상황을 다루기 위해서는 위 3개의 콜백함수 외에도 다른 콜백함수들을 작성해야한다. 모든 콜백 함수에 대한 더 자세한 내용은 다른 글에서 다두도록한다.


안드로이드는 기본 Fragment 클래스 대신, 쓸만한 몇 개의 서브 클래스를 제공한다.


DialogFragment

둥실둥실(?) 떠있는 다이얼로그를 띄울 때 사용한다. 액티비티 클래스에서 제공하던 Dialog Helper 함수 대신 사용하기 적절한데, 이유는 프레그먼트 다이얼로그를 액티비티에서 관리하는 프레그먼트들의 back stack 속에 포함시킬 수 있어서 사용자를 이전 프레그먼트로 돌아갈 수 있도록 할 수 있기 때문이다.


ListFragment

(SimpleCursorAdapter와 같은) 어댑터에 의해 관리되는 리스트 항목을 표출할 수 있는 ListActivity와  유사한 클래스다. 사용자 클릭을 처리하는 onListItemClick() 콜백 함수와 같이 리스트뷰를 관리할 수 있는 다양한 함수를 제공한다.


PreferencesFragment

ReferencesActivity와 유사하게 리스트 형태로 환경설정 계층을 표출할 수 있다. 우리의 앱에서 환경설정 액티비티를 제공할 때 유용하다.






액티비티가 동작중일 때의 프레그먼트의 생명주기

출처 : http://developer.android.com/guide/components/fragments.html




UI 추가하기 (Adding a user interface)


프레그먼트는 보통 액티비티의 한 부분을 차지하면서 액티비티에 자신의 레이아웃을 제공한다.


프레그먼트를 위한 레이아웃을 반영하려면 onCreateView() 콜백함수를 구현해야하는데, 위에서 설명한대로 이 함수는 프레그먼트가 레이아웃을 그리는 시점에 시스템에서 호출하는 콜백함수다. 이 함수를 구현할 때는 프레그먼트 레이아웃의 루트가 될 View를 리턴해야 한다.


주의사항 : ListFragment의 서브클래스인 경우, onCreateView()에서 디폴트로 ListView를 리턴하기 때문에 별도의 추가 구현이 필요없다.


onCreateView()에서 레이아웃을 리턴하려면, XML로 정의해둔 레이아웃 리소스를 이용하여 inflate 할 수 있다. 이 과정을 쉽게 할 수 있도록 onCreateView()가 호출될 때 LayoutInflater 오브젝트가 전달된다.


아래 코드는 example_fragment.xml 파일을 로드하는 프레그먼트의 서브클래스의 사용 예이다.


public static class ExampleFragment extends Fragment {
   
@Override
   
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             
Bundle savedInstanceState) {
       
// Inflate the layout for this fragment
       
return inflater.inflate(R.layout.example_fragment, container, false);
   
}
}


onCreateView()로 전달된 container 파라메터는 액티비티가 소유한 부모 ViewGroup인데, 이 그룹에 프레그먼트가 속하게 되는 것이다. 또다른 파라메터 savedInstanceState는 Bundle 오브젝트인데, 프레그먼트가 resume될 때, 프레그먼트의 이전 인스턴스 데이터가 담겨있다. (이에 대한 내용은 Handling the Fragment Lifecycle 섹션에서 자세히 다루도록 하자)


inflate() 함수를 호출하려면 3개의 매개변수가 필요하다.

  • inflate 하려는 레이아웃의 리소스 ID
  • inflate된 레이아웃의 부모가 될 ViewGroup. container(ViewGroup)객체를 전달하는 것은 매우 중요한데, 이는 시스템이 레이아웃 파라메터가 inflate되었을 때 이 녀석의 루트뷰(레이아웃이 속할 부모뷰에 의해 정의된)가 되도록 적용시켜야하기 때문이다. 
  • inflate 작업이 진행되는 동안, inflate된 레이아웃이 ViewGroup(두 번째 파라메터)에 적용(attatched)되어야 하는지 여부를 판단하는 boolean값. (여기서는 시스템이 이미 inflate된 레이아웃을 container에 집어넣었기 때문에 false값을 부여했다. 만약, 이 값을 true로 하면, 맨 마지막 레이아웃에서 불필요한 뷰 그룹을 생성하게 된다.)

여기까지 우리는 레이아웃을 갖는 프레그먼트를 만드는 법을 배웠다. 이제는 액티비티에 프레그먼트를 집어넣는 방법에 대해 알아보자.




액티비티에 프레그먼트 넣기(Adding a fragment to an activity)


일반적으로, 프레그먼트는 액티비티의 전체 뷰 계층의 일부로 포함되어있는 상위 액티비티에 UI 요소를 제공한다. 액티비티 레이아웃에 프레그먼트를 추가하는 방법에는 2가지가 있다.


  • 액티비티 레이아웃 파일에 fragment 선언하는 방법

이 경우, view에서 했던 것 처럼 레이아웃 속성을 작성할 수 있다. 아래는 두 개의 프레그먼트를 갖는 액티비티의 레이아웃 파일의 예시이다.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   
android:orientation="horizontal"
   
android:layout_width="match_parent"
   
android:layout_height="match_parent">
   
<fragment android:name="com.example.news.ArticleListFragment"
           
android:id="@+id/list"
           
android:layout_weight="1"
           
android:layout_width="0dp"
           
android:layout_height="match_parent" />
   
<fragment android:name="com.example.news.ArticleReaderFragment"
           
android:id="@+id/viewer"
           
android:layout_weight="2"
           
android:layout_width="0dp"
           
android:layout_height="match_parent" />
</LinearLayout>


<fragment> 안에 있는 "android:name" 속성은 Fragment 클래스가 레이아웃에서 인스턴스화되는 것을 의미한다. 시스템이 액티비티 레이아웃을 생성할 때, 레이아웃안에 정의된 각각의 프레그먼트의 인스턴스를 만들고 프레그먼트 레이아웃을 반환하도록 각 프레그먼트의 onCreateView()함수를 호출한다. 시스템은 프레그먼트에서 직접 반환한 View를 <fragment> 요소를 대체하여 추가한다.



주의 : 액티비티가 재실행되는 경우 프레그먼트를 복구하기 위해 시스템에서 사용하는 프레그먼트의 유니크한 ID가 필요하다. (또한, 프레그먼트 삭제와 같은 조작을 할 때도 프레그먼트를 구분하는데 사용할 수도 있다.) 프레그먼트를 위한 ID를 부여하는데는 3가지 방법이 있다.

    • "android:id" 속성을 통해 유니크한 ID를 부여한다.
    • "android:tag" 속성을  통해 유니크한 문자열를 부여한다.
    • 위의 두 개를 모두 부여하지 않는 경우, 시스템은 컨테이너뷰의 ID를 사용한다.

  • 직접 구현을 통해 이미 생성되어 있는 ViewGroup에 프레그먼트를 추가하는 방법

액티비티가 실행되는 동안에 우리는 액티비티 레이아웃에 프레그먼트를 추가할 수 있다. 이를 위해 간단하게 프레그먼트가 위치할 ViewGroup을 지정하면 된다.

프레그먼트 추가, 삭제, 교체 등과 같은 액비티비 내에서 프레그먼트를 조작하려면 반드시 FragmentTransaction에서 제공하는 API들을 사용해야한다. FragmentTransaction의 인스턴스는 Activity로 부터 얻을 수 있는데, 아래처럼 구현하면 된다.


FragmentManager fragmentManager = getFragmentManager()
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();


이제, 추가할 프레그먼트와 이를 포함할 뷰를 지정해주는 add()함수를 통해 프레그먼트를 추가할 수 있다.

아래 코드를 보자.


ExampleFragment fragment = new ExampleFragment();
fragmentTransaction
.add(R.id.fragment_container, fragment);
fragmentTransaction
.commit();


add()함수로 전달하는 첫번째 인자는 프레그먼트가 위치해야할 ViewGroup의 리소스 ID이고, 두번째 파라메터는 추가할 프레그먼트 객체다.

FragmentTransaction으로 변경할 내용을 완료했으면 실제 적용을 시키기 위해 commit()함수를 반드시 호출해야한다.



UI없는 프레그먼트 추가하기 (Adding a fragment without a UI)


위에서는 액티비티에 UI를 제공할 목적으로 프레그먼트를 만드는 법에 대해 설명했다. 하지만, 별도의 UI를 표출하지 않고 액티비티의 백그라운드 작업만을 수행하도록  만들 수도 있다.


UI없는 프레그먼트를 추가하려면 액티비티내에서 add(Fragment, String) 함수를 이용하여 추가하면 된다. (View ID보다는 유니크한 Tag 문자열을 부여하는 것이 더 좋다) 이 함수는 프레그먼트를 추가하긴 하지만 액티비티 레이아웃에 있는 뷰와는 연결되지 않기 때문에 onCreateView()함수가 호출되지 않는다. 즉, 이 콜백함수를 구현하지 않아도 된다.


프레그먼트에 문자열 Tag를 부여하는 것은 UI없는 프레그먼트만을 위한 것은 아니다. (UI를 갖는 프레그먼트에도 문자열 Tag를 부여할 수 있다.) 하지만, UI가 없는 프레그먼트의 경우라면 이 문자열 Tag는 해당 프레그먼트를 식별할 수 있는 유일한 정보다. 만약, 구동 중에 액티비티로부터 해당 프레그먼트 객체를 받아오고 싶다면 findFragmentByTag()함수를 사용하면 된다.


UI없이 백그라운드 작업을 수행하는 프레그먼트가 포함된 액티비티 클래스를 참고하려면 FragmentRetainInstance.java 샘플 코드를 참고하자.




[원문보기]




Posted by 데브로망스

댓글을 달아 주세요

  1. lee 2014.01.28 15:59  댓글주소  수정/삭제  댓글쓰기

    번역 감사합니다.

google-play-services_lib 업데이트 후, 죽는 문제



가끔씩 Android SDK Manger 버튼 눌러보면, SDK 등등이 업데이트 되었다고 체크되어 있다. 경험상 프로젝트가 진행중일 때는 절대로 업데이트하지 말아야 하는데, 이유는... 잘 동작하던 앱이 "죽는다"
최근에도 이런 경험을 했는데, 다행히 일 없이 널널할 때여서 뭔 일인가 꼼꼼히 살펴봤다.

이번에 업데이트를 한 것은 GCM이나 Google Map v2를 위해 추가해야하는 구글플레이서비스 라이브러리(google-play-services_lib)였는데, 처음 본 에러메시지는 아래와 같다.

01-13 14:27:16.976: E/AndroidRuntime(7367): Caused by: java.lang.IllegalStateException: The meta-data tag in your app's AndroidManifest.xml does not have the right value.  Expected 4030500 but found 0.  You must have the following declaration within the <application> element:     <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />


관련해서 찾아보니, 필수 meta-tag가 추가되었단다.

간단하다. Google Map v2의 Key를 넣는 것 처럼 매니페스트에 아래 내용을 추가하면 이 문제는 해결된다.


<Application 
...
    <meta-data
        android:name="com.google.android.gms.version"
        android:value="@integer/google_play_services_version" />
...
</Application>


그런데, 비슷한 문제가 또 발생한다.

int iErrorCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);


여기서 에러코드 "2"를 뱉어낸다.


public static final int SERVICE_VERSION_UPDATE_REQUIRED The installed version of Google Play services is out of date. The calling activity should pass this error code to getErrorDialog(int, Activity, int) to get a localized error dialog that will resolve the error when shown. Constant Value: 2 (0x00000002) Details... 


버전이 낮아서 발생하는 문제란다. 
이때 출력된 로그를 보면, 자기는 4030500을 원했는데, 내꺼는 3266136이란다.

W/GooglePlayServicesUtil(4728): Google Play services out of date. Requires 4030500 but found 3266136 


4030500이란 숫자는 뭔가 찾아봤다.

[SDK]\extras\google\google_play_services\libproject\google-play-services_lib\res\values

폴더에 version.xml파일이 있는데, 열어보면 4030500이라는 버전이 적혀있다.

<?xml version="1.0" encoding="utf-8"?> 
<resources>
    <integer name="google_play_services_version">4030500</integer>
</resources>


3266136은 못찾겠다. 어디에 적혀있었을텐데, 버전을 관리하기 위에서 이번부터 리소스에 넣기로 (안드로이드 OS개발자가...) 결정했나보다. 


어쨌든, 그렇다면 해결책은? 두 달전에 이 이슈가 발생했을 때는 해결책이 없었다. 구글플레이서비스 앱은 아직 4030500을  지원하지 않는데, 라이브러리만 4030500으로 올려놓고 먼저 배포한 구글직원의 잘못이었다.

새로운 라이브러리를 삭제하고 다시 잘 동작하던 라이브러리로 원복해서 사용할 수 밖에 없었다.


이 문제를 접한건 2013년 11월 초였는데, 지난 주, 2014년 1월 9일에 구글 플레이 서비스가 업데이트 되었다는 내용이 안드로이드 공식 블로그에 올라왔다. 구글플레이스토어 앱의 버전을 확인하니 4.5.10이란다. 업데이트 되었다. (이전 버전은 4.4.21, 구글플레이스토어 앱은 별도 설치없이 나 몰래 자동업그레이드 된다.) 


다시, 최신 라이브러리를 꺼내서 적용했더니, 잘 동작한다. 해결됐다. 새로운 구글플레이스토어 앱 어딘가에 "4030500"이란 버전명이 적혀있나보다. 이러한 문제가 발생해도 공식적으로 아무런 조언도 해주지 않는 구글이 참 섭섭하지만 어쨌든 일은 해야하니까.. "프로젝트 진행중에는 왠만하면 ADT나 SDK 관련 업데이트를 하지말자!"



Posted by 데브로망스

댓글을 달아 주세요

  1. yong 2016.02.01 10:21  댓글주소  수정/삭제  댓글쓰기

    앱 내에 버전 하드코딩 하신거같은데요. 한번 찾아보세요. 그거 지우면 정상적으로 돌아갈겁니다.

  2. yong 2016.02.01 10:22  댓글주소  수정/삭제  댓글쓰기

    <integer name="google_play_services_version">3266136</integer> 이렇게 하드코딩 되어있을거에요

    • 데브로망스 2016.02.01 10:43 신고  댓글주소  수정/삭제

      아! 하드코딩했던게 아닙니다.
      저때, 발생했던 내용은 본문 그대로입니다.
      구글에서 버전 관리를 잘못해서 발생했다고 판단할 수 밖에 없었습니다.
      (당시, 이런저런 동일 발생 글을 보고 판단했던 기억이 있네요.)

      다행히 최근에는 이런 문제가 발생하지 않네요. ^^

      댓글 감사드립니다!

프레그먼트의 디자인 철학(Design Philosophy)



태블릿과 같은 큰 화면에서 동적이면서 유연성있는 UI 디자인을 지원하기 위해 안드로이드 3.0(API Level 11)부터 프레그먼트를 지원한다. 태블릿은 스마트폰보다 화면 사이즈가 크기때문에 UI 요소를 통합하거나 교체할 수 있는 여유 공간이 있다. 프레그먼트는 뷰계층의 복잡한 변경을 관리하지 않고도 화면을 설계할 수 있게 해준다. 액티비티의 레이아웃을 프레그먼트 단위로 나눌 수 있게되면서 우리는 런타임시에도 액티비티의 외관을 변경할 수 있고 또한, 변경된 내용을 액티비티가 관리하는 백스택으로 보존할 수도 있게 되었다.


예를 들면, 뉴스 앱에서 왼쪽 프레그먼트에는 뉴스 목록을, 오른쪽에는 해당 기사 내용을 보여줄 수 있다. 자세히 설명하면 두 프레그먼트를 하나의 액티비티에서 나란히 보여주고 각각의 프레그먼트는 자신의 생명주기 콜백함수를 정의해서 사용자 입력 이벤트를 직접 처리할 수 있다. 그래서, 아래 그림과 같이 한 액티비티에서는 뉴스 목록을 보여주고 다른 액티비티에서 선택된 뉴스 기사를 보도록 구현하는 대신 사용자는 목록을 선택하고 같은 액티비티에서 바로 기사를 읽도록 구성할 수 있다.



프레그먼트로 정의된 두 개의 UI 모듈이 태블릿에서는 결합된 형태로, 스마트폰에서는 분리된 형태로 보여주는  예시

[이미지 출처 : http://developer.android.com/guide/components/fragments.html#Design]


앱이 태블릿에서 실행될 때는 액티비티A에 두 개의 프레그먼트를 넣을 수 있다. 반면에 스마트폰에서는 공간의 여유가 없기 때문에 액비티비A에는 뉴스목록을 위한 프레그먼트만 넣고 사용자가 항목을 선택했을 때 액티비티B를 띄우면서 그 안에 뉴스 기사를 위한 프레그먼트를 넣도록 구현한다. 결과적으로 앱은 다양한 결합방법을 통해 프레그먼트를 재사용해서 태블릿과 스마트폰을 동시에 지원할 수 있다.



[원문보기]


Posted by 데브로망스

댓글을 달아 주세요

  1. 어제전화받은사람 2014.01.10 18:09  댓글주소  수정/삭제  댓글쓰기

    ㅋㅋㅋ

  2. 데브로망스 2014.01.10 18:30 신고  댓글주소  수정/삭제  댓글쓰기

    @어제전화받은그사람 웰컴! ㅎㅎㅎ

프레그먼트(Fragments)란?



프레그먼트는 액티비티 내에서의 행위 또는 UI의 일부를 대체할 수 있다. 우리는 여러개의 프레그먼트를 결합해서 하나의 액티비티를 여러개의 판으로 나뉘어진 UI를 구성할 수 있다. 또한, 정의한 프레그먼트를 다른 액티비티에서 재사용할 수도 있다. 프레그먼트는 액티비티의 모듈화된 구역이라고 이해할 수 있다. 따라서 프레그먼트는 독립된 라이프 사이클을 갖고, 자신의 입력 이벤트를 수신하며 액티비티가 살아있는 중간에 추가되거나 삭제(다른 액티비티에서 재사용할 수 있는 일종의 서브 액티비티 같은 것)될 수 있다.


프레그먼트는 항상 액티비티에 속하게되고 생명주기는 바로 그 액티비티의 생명주기에 직접적으로 영향을 받게된다. 예를 들어, 액티비티가 멈출때(Paused) 그 안에 속한 모든 프레그먼트는 멈추게 되고, 닫힐때(Destroyed)도 마찬가지다. 하지만, 액티비티가 동작할 때(Resumed 상태일 때)는 각각의 프레그먼트를 독립적으로 조작(추가/삭제)할 수 있다. 또한 프레그먼트 조작을 수행할 때 액티비티에서 관리하는 백스택(back stack)에 추가할 수도 있다. (액티비티에 속한 각각의 백스택 항목들은 프레그먼트가 조작된 기록이다.) 백스택은 백버튼을 눌렀을 때, 뒤로가기와 같은 프레그먼트 조작을 가능하게 해준다.


프레그먼트를 액티비티 레이아웃의 한 부분으로 추가하면, 이 프레그먼트는 액티비티의 뷰계층에 속해있는 ViewGroup에 존재하게 되고, 뷰 레이아웃을 스스로 정의한다. 프레그먼트를 액티비티 레이아웃에 포함시키는 방법은 두 가지가 있는데, 하나는 액티비티 레이아웃 파일에 프레그먼트를 선언(<fragment>태그)하는 방법이고, 다른 하나는 ViewGroup 객체에 Java Code로 추가하는 것이다. 하지만, 프레그먼트라고 해서 반드시 액티비티 레이아웃의 한 부분이 될 필요는 없다. UI없이 액티비티를 위한 숨은 작업을 하도록 구현할 수 있다.



[원문보기]



Posted by 데브로망스

댓글을 달아 주세요