[Android] Activity 살펴보기 : ComponentActivity
현재 우리가 자주 사용하는 형태의 Activity 계층 구성도를 한번 살펴보겠습니다.
이중에서 오늘 포스팅 할 주된 내용은 androidx.activity.ComponentActivity 입니다. Compose 를 Ui toolkit 으로 사용하고 계신다면, 이 클래스를 상속하실 겁니다. 생각보다 많은 기능을 제공해주고 있는데, 하나씩 살펴보겠습니다.
ComponentActivity
먼저 어떤 인터페이스를 구현하고 있는지 살펴보겠습니다.
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
ContextAware,
LifecycleOwner,
ViewModelStoreOwner,
HasDefaultViewModelProviderFactory,
SavedStateRegistryOwner,
OnBackPressedDispatcherOwner,
ActivityResultRegistryOwner,
ActivityResultCaller,
OnConfigurationChangedProvider,
OnTrimMemoryProvider,
OnNewIntentProvider,
OnMultiWindowModeChangedProvider,
OnPictureInPictureModeChangedProvider,
MenuHost,
FullyDrawnReporterOwner {
상당히 많은 인터페이스를 구현하고 있는데, 특정 기능에 대해 관심사가 분리되어 있으므로 나뉘어진 인터페이스들을 묶어보면 이렇게 분류할 수 있습니다.
- ContextAware : Context 가 유효한 시점을 판단
- LifecycleOwner : Lifecycle
- ViewModel 관련
- ViewModelStoreOwner : ViewModelStore(뷰모델 인스턴스 저장)
- HasDefaultViewModelProviderFactory : 기본 ViewModelProviderFactory 구현체(SavedStateViewModel)
- SavedStateRegistryOwner : SavedStateRegistry(시스템에 의한 프로세스 메모리 제거시 상태 저장 및 복원)
- OnBackPressedDispatcherOwner : 시스템의 뒤로가기 핸들링
- ActivityResult(화면 또는 권한 요청 과 응답)
- ActivityResultRegistryOwner : ActivityResultRegistry(화면 또는 권한 요청 및 응답 처리)
- ActivityResultCaller : ActivityResult*** 통합 및 ActivityResultLauncher 반환
- OnConfigurationChangedProvider : 구성 변경 콜백 처리
- OnTrimMemoryProvider : 메모리에서 제거되는 타이밍의 콜백 처리
- OnNewIntentProvider : OnNewIntent() 호출시 콜백 처리
- OnMultiWindowModeChangedProvider : 멀티 윈도우 전환시 콜백 처리
- OnPictureInPictureModeChangedProvider : PiP 모드 전환시 콜백 처리
- MenuHost : ActionBar 메뉴 처리
- FullyDrawnReporterOwner : FullyDrawnReporter(TTFD 타이밍 제어 및 완료 후 콜백 실행)
익숙한 클래스명들이 보입니다. 특히 상태관리 관련해서는 이미 Android 상태 관리 #1 ViewModel 와 Android 상태 관리 #2 SavedState 에서 다루었던 내용입니다.
그외에 나머지들을 하나씩 살펴보겠습니다.
ContextAware
ContextAware 인터페이스는 이름에서 알 수 있듯이 옵저버 패턴을 이용하여 Context 가 활성화 된 순간을 알 수 있도록 돕는 기능을 제공합니다. 내부적으로는 Context가 유효한 시점은 ComponentActivity 내에서 Activity#onCreate() 가 호출되기 직전을 의미하고, 이 때 특정 작업을 실행해주기 위한 OnContextAvailableListener 콜백을 제공하고 있습니다. 이슈 트래커 에서 볼 수 있듯이, onCreate 시점 직전에 특정 작업들을 해주어야 하는 경우를 위해 제공된 것으로 보입니다.
코드를 살펴보겠습니다.
public interface ContextAware {
// 이값은 nullable 하며, 이용 가능한 경우 context 를 반환하고, 그렇지 않은 경우 null 을 반환합니다.
@Nullable
Context peekAvailableContext();
// context 가 이용 가능한 시점에 특정 기능을 수행해 줄 콜백을 등록합니다.
void addOnContextAvailableListener(@NonNull OnContextAvailableListener listener);
// 등록한 콜백을 제거합니다.
void removeOnContextAvailableListener(@NonNull OnContextAvailableListener listener);
}
해당 인터페이스의 구현은 androidx.activity.ComponentActivity 가 직접 가지지 않고, ContextAware 인터페이스를 직접 구현하지는 않지만 구현 로직을 가지는 헬퍼클래스를 의존하고 위임하는 패턴을 사용하고 있습니다. 이러한 패턴은 MenuHost 인터페이스와 MenuHostHelper 클래스에서도 사용된걸 확인 할 수 있는데, 관심사를 분리함으로써 구현부에 대해서 캡슐화를 하기 위해 사용된것으로 보입니다.
ContextAware 인터페이스로 개발자가 사용할 수 있는 기능들을 정의 및 제한하고, ContextAwareHelper 에서는 이와 더불어 그외의 내부적으로 사용하는 기능들을 정의함으로써 캡슐화를 하고 있습니다.
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
ContextAware, {
final ContextAwareHelper mContextAwareHelper = new ContextAwareHelper();
public ComponentActivity() {
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
// Clear out the available context
mContextAwareHelper.clearAvailableContext();
// And clear the ViewModelStore
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
mReportFullyDrawnExecutor.activityDestroyed();
}
}
});
}
@OptIn(markerClass = BuildCompat.PrereleaseSdkCheck.class)
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// Restore the Saved State first so that it is available to
// OnContextAvailableListener instances
mSavedStateRegistryController.performRestore(savedInstanceState);
mContextAwareHelper.dispatchOnContextAvailable(this);
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public Context peekAvailableContext() {
return mContextAwareHelper.peekAvailableContext();
}
@Override
public final void addOnContextAvailableListener(
@NonNull OnContextAvailableListener listener) {
mContextAwareHelper.addOnContextAvailableListener(listener);
}
@Override
public final void removeOnContextAvailableListener(
@NonNull OnContextAvailableListener listener) {
mContextAwareHelper.removeOnContextAvailableListener(listener);
}
}
ComponentActivity 에서 ContextAware 와 관련된 부분들만 발췌했습니다. mContextAwareHelper 라는 프로퍼티로 ContextHelper 인스턴스를 생성하고 있으며, ContextAware 인터페이스의 구현을 모두 위임하고 있습니다.
또한 위에서 설명한대로 ComponentActivity#onCreate() 에서 Activity#onCreate() 가 호출되기 직전에 상태 복원 후 ContextAwareHelper#dispatchOnContextAvailable() 을 통해 Context 가 유효함을 알리고 등록된 콜백들을 실행할 것 입니다.
이후 activity 가 onDestroy() 호출로 소멸되면, context 가 더이상 유효하지 않기 때문에 ContextAwareHelper#clearAvailableContext() 로 초기화 합니다.
이제, 핵심 로직이 위치하는 ContextAwareHelper 를 살펴보겠습니다.
public final class ContextAwareHelper {
private final Set<OnContextAvailableListener> mListeners = new CopyOnWriteArraySet<>();
private volatile Context mContext;
public ContextAwareHelper() {
}
@Nullable
public Context peekAvailableContext() {
return mContext;
}
public void addOnContextAvailableListener(@NonNull OnContextAvailableListener listener) {
if (mContext != null) {
listener.onContextAvailable(mContext);
}
mListeners.add(listener);
}
public void removeOnContextAvailableListener(@NonNull OnContextAvailableListener listener) {
mListeners.remove(listener);
}
public void dispatchOnContextAvailable(@NonNull Context context) {
mContext = context;
for (OnContextAvailableListener listener : mListeners) {
listener.onContextAvailable(context);
}
}
public void clearAvailableContext() {
mContext = null;
}
}
옵저버 패턴을 이용하여 ComponentActivity 로 부터 context 를 할당받은 뒤, context 가 유효한 시점에 OnContextAvailableListener#onContextAvailable(Context) 로 등록된 콜백들을 실행합니다.
mListeners 는 CopyOnWriteArraySet
옵저버 패턴의 경우 언제 읽기 작업이 수행 될지 알 수 없고 빈번하게 일어날 수 있으며 동시에 콜백을 등록할 수 있기 때문에 대부분 이러한 thread safe 한 클래스들을 이용합니다.
이외에도 componentActivity 가 구현하는 OnConfigurationChangedProvider, OnTrimMemoryProvider, OnNewIntentProvider, OnMultiWindowModeChangedProvider, OnPictureInPictureModeChangedProvider 와 같이 옵저버 패턴을 이용하는 인터페이스들도 CopyOnWriteArrayList 타입을 이용하고 있습니다.
LifecycleOwner
public interface LifecycleOwner {
public val lifecycle: Lifecycle
}
LifecycleOwner 인터페이스는 단순히 Lifecycle 추상클래스 타입 인스턴스를 반환하는 책임을 갖는 함수형 인터페이스 입니다. ComponentActivity 는 이 인터페이스를 구현하고, 내부적으로 Lifecycle 추상클래스를 상속하는 LifecycleRegistry 클래스 인스턴스를 생성하고 반환합니다.
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
LifecycleOwner, {
private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
public ComponentActivity() {
Lifecycle lifecycle = getLifecycle();
if (lifecycle == null) {
throw new IllegalStateException("getLifecycle() returned null in ComponentActivity's "
+ "constructor. Please make sure you are lazily constructing your Lifecycle "
+ "in the first call to getLifecycle() rather than relying on field "
+ "initialization.");
}
if (Build.VERSION.SDK_INT >= 19) {
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_STOP) {
Window window = getWindow();
final View decor = window != null ? window.peekDecorView() : null;
if (decor != null) {
Api19Impl.cancelPendingInputEvents(decor);
}
}
}
});
}
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
// Clear out the available context
mContextAwareHelper.clearAvailableContext();
// And clear the ViewModelStore
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
mReportFullyDrawnExecutor.activityDestroyed();
}
}
});
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
ensureViewModelStore();
getLifecycle().removeObserver(this);
}
});
if (19 <= SDK_INT && SDK_INT <= 23) {
getLifecycle().addObserver(new ImmLeaksCleaner(this));
}
}
@NonNull
@Override
public Lifecycle getLifecycle() {
return mLifecycleRegistry;
}
}
LifecycleOwner 의 getLifecycle() 사용과 LifecycleRegistry 관련 코드만 발췌했습니다. Activity 의 Lifecycle 변화에 따라 특정 작업을 생성해주기 위해 observer 패턴을 사용하고 있습니다. LifecycleObserver 인터페이스의 구현체로 콜백을 등록할 수 있습니다. LifecycleObserver 구현체는 DefaultLifecycleObserver 와 LifecycleEventObserver 로 두가지가 존재합니다.
public interface DefaultLifecycleObserver : LifecycleObserver {
public fun onCreate(owner: LifecycleOwner) {}
public fun onStart(owner: LifecycleOwner) {}
public fun onResume(owner: LifecycleOwner) {}
public fun onPause(owner: LifecycleOwner) {}
public fun onStop(owner: LifecycleOwner) {}
public fun onDestroy(owner: LifecycleOwner) {}
}
public fun interface LifecycleEventObserver : LifecycleObserver {
/**
* Called when a state transition event happens.
*
* @param source The source of the event
* @param event The event
*/
public fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event)
}
DefaultLifecycleObserver 는 Lifecycle.Event 가 호출될 때 마다 대응되는 콜백 메소드를 실행시켜 줍니다. 위에서 다룬 바와 같이 onCreate, onStart, onResume 은 owner 의 콜백이 수행된 후에 호출되며 onPause, onStop, onDestroy 는 owner 의 콜백이 수행되기 전에 호출됩니다.
이와 달리 LifecycleEventObserver 는 모든 lifecycle 의 변화 마다 호출됩니다. 따라서 구현부에서 lifecycle.State 나 Lifecycle.Event 에 분기처리 하여 원하는 로직을 수행시키도록 구현할 수 있습니다.
만약, 두가지의 observer 를 모두 구현하는 Observer 클래스를 등록한다면 DefaultLifecycleObserver 가 먼저 호출되고 난 다음 LifecycleEventObserver 가 호출됩니다.
open class LifecycleRegistry private constructor(
provider: LifecycleOwner,
private val enforceMainThread: Boolean
) : Lifecycle() {
/**
* Custom list that keeps observers and can handle removals / additions during traversal.
*
* Invariant: at any moment of time for observer1 & observer2:
* if addition_order(observer1) < addition_order(observer2), then
* state(observer1) >= state(observer2),
*/
private var observerMap = FastSafeIterableMap<LifecycleObserver, ObserverWithState>()
/**
* Current state
*/
private var state: State = State.INITIALIZED
/**
* The provider that owns this Lifecycle.
* Only WeakReference on LifecycleOwner is kept, so if somebody leaks Lifecycle, they won't leak
* the whole Fragment / Activity. However, to leak Lifecycle object isn't great idea neither,
* because it keeps strong references on all other listeners, so you'll leak all of them as
* well.
*/
private val lifecycleOwner: WeakReference<LifecycleOwner>
private var addingObserverCounter = 0
private var handlingEvent = false
private var newEventOccurred = false
// we have to keep it for cases:
// void onStart() {
// mRegistry.removeObserver(this);
// mRegistry.add(newObserver);
// }
// newObserver should be brought only to CREATED state during the execution of
// this onStart method. our invariant with observerMap doesn't help, because parent observer
// is no longer in the map.
private var parentStates = ArrayList<State>()
/**
* Creates a new LifecycleRegistry for the given provider.
*
* You should usually create this inside your LifecycleOwner class's constructor and hold
* onto the same instance.
*
* @param provider The owner LifecycleOwner
*/
constructor(provider: LifecycleOwner) : this(provider, true)
init {
lifecycleOwner = WeakReference(provider)
}
/**
* Adds a LifecycleObserver that will be notified when the LifecycleOwner changes
* state.
*
* The given observer will be brought to the current state of the LifecycleOwner.
* For example, if the LifecycleOwner is in [Lifecycle.State.STARTED] state, the given observer
* will receive [Lifecycle.Event.ON_CREATE], [Lifecycle.Event.ON_START] events.
*
* @param observer The observer to notify.
*
* @throws IllegalStateException if no event up from observer's initial state
*/
override fun addObserver(observer: LifecycleObserver) {
enforceMainThreadIfNeeded("addObserver")
val initialState = if (state == State.DESTROYED) State.DESTROYED else State.INITIALIZED
val statefulObserver = ObserverWithState(observer, initialState)
val previous = observerMap.putIfAbsent(observer, statefulObserver)
if (previous != null) {
return
}
val lifecycleOwner = lifecycleOwner.get()
?: // it is null we should be destroyed. Fallback quickly
return
val isReentrance = addingObserverCounter != 0 || handlingEvent
var targetState = calculateTargetState(observer)
addingObserverCounter++
while (statefulObserver.state < targetState && observerMap.contains(observer)
) {
pushParentState(statefulObserver.state)
val event = Event.upFrom(statefulObserver.state)
?: throw IllegalStateException("no event up from ${statefulObserver.state}")
statefulObserver.dispatchEvent(lifecycleOwner, event)
popParentState()
// mState / subling may have been changed recalculate
targetState = calculateTargetState(observer)
}
if (!isReentrance) {
// we do sync only on the top level.
sync()
}
addingObserverCounter--
}
private fun popParentState() {
parentStates.removeAt(parentStates.size - 1)
}
private fun pushParentState(state: State) {
parentStates.add(state)
}
override fun removeObserver(observer: LifecycleObserver) {
enforceMainThreadIfNeeded("removeObserver")
// we consciously decided not to send destruction events here in opposition to addObserver.
// Our reasons for that:
// 1. These events haven't yet happened at all. In contrast to events in addObservers, that
// actually occurred but earlier.
// 2. There are cases when removeObserver happens as a consequence of some kind of fatal
// event. If removeObserver method sends destruction events, then a clean up routine becomes
// more cumbersome. More specific example of that is: your LifecycleObserver listens for
// a web connection, in the usual routine in OnStop method you report to a server that a
// session has just ended and you close the connection. Now let's assume now that you
// lost an internet and as a result you removed this observer. If you get destruction
// events in removeObserver, you should have a special case in your onStop method that
// checks if your web connection died and you shouldn't try to report anything to a server.
observerMap.remove(observer)
}
/**
* The number of observers.
*
* @return The number of observers.
*/
open val observerCount: Int
get() {
enforceMainThreadIfNeeded("getObserverCount")
return observerMap.size()
}
@SuppressLint("RestrictedApi")
private fun enforceMainThreadIfNeeded(methodName: String) {
if (enforceMainThread) {
check(ArchTaskExecutor.getInstance().isMainThread) {
("Method $methodName must be called on the main thread")
}
}
}
internal class ObserverWithState(observer: LifecycleObserver?, initialState: State) {
var state: State
var lifecycleObserver: LifecycleEventObserver
init {
lifecycleObserver = Lifecycling.lifecycleEventObserver(observer!!)
state = initialState
}
fun dispatchEvent(owner: LifecycleOwner?, event: Event) {
val newState = event.targetState
state = min(state, newState)
lifecycleObserver.onStateChanged(owner!!, event)
state = newState
}
}
}
ComponentActivity 에서 생성되는 Lifecycle 구현체인 LifecycleRegistry 를 일부 발췌했습니다. 가장 보이는 FastSafeIterableMap 타입의 observerMap 프로퍼티로 LifecycleObserver 들을 유지합니다.
public class FastSafeIterableMap<K, V> extends SafeIterableMap<K, V> {
private final HashMap<K, Entry<K, V>> mHashMap = new HashMap<>();
@Nullable
@SuppressWarnings("HiddenTypeParameter")
@Override
protected Entry<K, V> get(K k) {
return mHashMap.get(k);
}
@Override
public V putIfAbsent(@NonNull K key, @NonNull V v) {
Entry<K, V> current = get(key);
if (current != null) {
return current.mValue;
}
mHashMap.put(key, put(key, v));
return null;
}
@Override
public V remove(@NonNull K key) {
V removed = super.remove(key);
mHashMap.remove(key);
return removed;
}
/**
* Returns {@code true} if this map contains a mapping for the specified
* key.
*/
public boolean contains(K key) {
return mHashMap.containsKey(key);
}
/**
* Return an entry added to prior to an entry associated with the given key.
*
* @param k the key
*/
@Nullable
public Map.Entry<K, V> ceil(K k) {
if (contains(k)) {
return mHashMap.get(k).mPrevious;
}
return null;
}
}
public class SafeIterableMap<K, V> implements Iterable<Map.Entry<K, V>> {
@SuppressWarnings("WeakerAccess") /* synthetic access */
Entry<K, V> mStart;
private Entry<K, V> mEnd;
// using WeakHashMap over List<WeakReference>, so we don't have to manually remove
// WeakReferences that have null in them.
private final WeakHashMap<SupportRemove<K, V>, Boolean> mIterators = new WeakHashMap<>();
private int mSize = 0;
// 일부 생략
}
FastSafeIterableMap 타입은 순회중에 수정을 지원하는 LinkedHashMap 의 실제 구현보다 더 가벼운(덜 복잡한) 구현체입니다.
LinkedHashMap 은 HashMap 을 상속하고, 값 입력 순서를 유지하기 위해 LinkedList 특성을 사용하는 클래스입니다. Java 의 LinkedList 는 기본적으로 doubly-linked 구조를 가져서 single-Linked 보다 탐색에 더나은 성능을 가집니다.(n-1의 아이템을 탐색한다면, single 은 첫번째 아이템부터 끝까지 탐색하지만 doubly 는 맨마지막 요소의 head 로 탐색할 수 있으므로 최악의 경우 탐색 성능이 절반만큼 좋습니다.) HashMap 을 상속받기 때문에 기본적으로 thread-safe 하지 않습니다.
FastSafeIterableMap 은 SafeIterableMap 을 상속하고 내부적으로 HashMap 을 프로퍼티로 유지합니다. 두 클래스 모두 androidx.arch.core.internal 에 존재하는 안드로이드에서 만든 클래스입니다. SafeIterableMap 은 Iterable<Map.Entry<K, V» 을 구현하고, 앞과 뒤 요소에 대한 참조를 가져 Map 인 것 처럼 동작하는 LinkedList 역할을 수행합니다. 이 클래스의 목적도 순회 중에 수정을 지원하기 위함 이며, thread-safe 하지 않습니다.
FastSafeIterableMap 타입으로 LifecycleObserver 들을 순회중에도 수정 과 삭제를 할수있도록 만듭니다. 단, 이는 thread-safe 하지 않습니다. 따라서 LifecycleRegistry#enforceMainThreadIfNeeded() 를 사용하여 MainThread 에서만 접근할 수 있도록 제한하고 있습니다. 그외에 생략된 것들은 Lifecycle 의 상태나 이벤트를 다루고 Kotlinx.coroutines.flow 의 stateFlow 로 노출하는 등의 기능을 제공하고 있습니다.
ViewModel 관련 인터페이스들
ViewModel 관련해서는 이미 Android 상태 관리 #1 ViewModel 에서 다루었던 내용입니다. 자세한 내용은 챕터1~3 에서 상세하게 다루고 있으니 참고하시면 되겠습니다.
OnBackPressedDispatcherOwner
OnBackPressedDispatcherOwner 는 시스템의 Back 버튼을 눌러 뒤로가기를 핸들링 하기 위한 역할을 담당하는 OnBackPressedDispatcher 를 갖는 인터페이스 입니다. OnBackPressedDispatcher 는 observer 패턴을 이용하여 시스템 back key 이벤트가 발생했을 때 등록된 콜백을 실행시켜준 뒤, OnBackPressedDispatcher 생성자로 넘겨준 Runnable 구현체인 OnBackPressedDispatcher#fallbackOnBackPressed 를 실행시켜줍니다. 이 구현체는 ComponentActivity 에서 확인 가능하며, 단순히 super 클래스인 Activity#onBackPressed() 를 호출함으로써 내부적으로 시스템의 back 이벤트를 실행합니다.
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
OnBackPressedDispatcherOwner, {
private final OnBackPressedDispatcher mOnBackPressedDispatcher =
new OnBackPressedDispatcher(new Runnable() {
@SuppressWarnings("deprecation")
@Override
public void run() {
// Calling onBackPressed() on an Activity with its state saved can cause an
// error on devices on API levels before 26. We catch that specific error
// and throw all others.
try {
ComponentActivity.super.onBackPressed();
} catch (IllegalStateException e) {
if (!TextUtils.equals(e.getMessage(),
"Can not perform this action after onSaveInstanceState")) {
throw e;
}
}
}
});
}
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback,
ContentCaptureManager.ContentCaptureClient {
public void onBackPressed() {
if (mActionBar != null && mActionBar.collapseActionView()) {
return;
}
FragmentManager fragmentManager = mFragments.getFragmentManager();
if (!fragmentManager.isStateSaved() && fragmentManager.popBackStackImmediate()) {
return;
}
onBackInvoked();
}
private void onBackInvoked() {
// Inform activity task manager that the activity received a back press.
// This call allows ActivityTaskManager to intercept or move the task
// to the back when needed.
ActivityClient.getInstance().onBackPressed(mToken,
new RequestFinishCallback(new WeakReference<>(this)));
if (isTaskRoot()) {
getAutofillClientController().onActivityBackPressed(mIntent);
}
}
}
실제로 우리가 뒤로가기를 커스텀하기 위해 activity의 onBackPressedDispatcher 에 OnBackPressedCallback 의 구현체를 onBackPressedDispatcher#addCallback(lifecycleOwner, callback) 메소드로 등록하여 사용할 수 있습니다. 해당 메소드는 lifecycleOwner 를 함께 넘겨주어 화면이 보여지고 있는 경우(ON_START ~ ON_STOP) 활성화하고, 그렇지 않은경우 콜백을 취소시키고 제거하여 lifecycle-aware 하게 동작합니다.
class OnBackPressedDispatcher @JvmOverloads constructor(
private val fallbackOnBackPressed: Runnable? = null
) {
private val onBackPressedCallbacks = ArrayDeque<OnBackPressedCallback>()
private var enabledChangedCallback: (() -> Unit)? = null
private var onBackInvokedCallback: OnBackInvokedCallback? = null
private var invokedDispatcher: OnBackInvokedDispatcher? = null
private var backInvokedCallbackRegistered = false
@MainThread
fun addCallback(onBackPressedCallback: OnBackPressedCallback) {
addCancellableCallback(onBackPressedCallback)
}
@MainThread
fun addCallback(
owner: LifecycleOwner,
onBackPressedCallback: OnBackPressedCallback
) {
val lifecycle = owner.lifecycle
if (lifecycle.currentState === Lifecycle.State.DESTROYED) {
return
}
onBackPressedCallback.addCancellable(
LifecycleOnBackPressedCancellable(lifecycle, onBackPressedCallback)
)
if (Build.VERSION.SDK_INT >= 33) {
updateBackInvokedCallbackState()
onBackPressedCallback.enabledChangedCallback = enabledChangedCallback
}
}
}
몇가지 프로퍼티와 메소드만 발췌했습니다. 가장먼저 보이는 콜백들을 유지하기 위해 onBackPressedCallbacks 프로퍼티는 ArrayDeque 타입을 사용하고 있습니다. ArrayDeque 는 kotlin.collections 에 있으며 AbstractMutableList 추상 클래스를 상속하고 있습니다. 이름에서 알 수 있듯이 Duque 라는 자료구조를 사용했는데, 이는 기존의 Queue 가 FIFO 로써 한방향으로만 데이터를 집어넣고 꺼낼수 있었다면 Duque 는 양방향으로 데이터를 집어넣고 꺼낼 수 있습니다(앞 또는 맨뒤로 데이터를 집어넣고 꺼낼수있음). 또한 resizable-array 하여 내부적으로 배열로 데이터를 유지하되, capacity 를 넘어서는 수의 데이터가 삽입 요청되면 새로운 더 큰 배열을 만들고 값을 복사하여 가지는 클래스입니다.
배열은 List 와 달리 생성 시점에 크기를 지정해야하지만, 참조 지역성의 원리에 따라 메모리에 연속된 주소에 할당되기 때문에 탐색에서 더 성능이 더 좋습니다. 만약, 데이터 삽입시 capacity를 넘어서는 경우 새로운 capacity 만큼을 가진 배열 공간을 할당해야 합니다. 코틀린에서의 Array#sort() 는 DualPivotQuickSort 를 이용합니다.
Api 33 부터 추가된 predictive back gesture 를 내부적으로 compatible 하게 동작합니다. Api 33 이상 인지 체크후 OnBackInvokedDispatcher 와 OnBackInvokedCallback 을 이용하여 내부적으로 애니메이션 및 여러 디자인 컴포넌트들에 대한 back key 이벤트를 처리해줍니다.
ActivityResult(화면 또는 권한 요청 과 응답)
ActivityResult 는 Api 30 부터 새롭게 화면 요청 또는 권한 요청 후 처리에 대해 통합 솔루션으로 추가되었습니다. 기존의 방식에서는 Activity#startActivity() 또는 Activity#startActivityForResult() 와 Activity#requestPermissions() 으로 권한을 요청하고 Activity#onRequestPermissions() 로 응답받도록 분리되어 구현했지만 해당 방법들은 모두 deprecate 되고, ActivityResult 를 사용해야 합니다. Api 30 미만에서 작업했던 코드들은 ActivityResult 로 migration 해야하며, 내부적으로 compatable 하게 동작하도록 구현되어있기 때문에 개발자가 Api 30 미만 버전에 대응시켜주기 위한 별도의 코드작업은 하지 않아도 됩니다.
ActivityResult*** 는 크게 3가지로 관심사가 분리되어있습니다. 핵심 비즈니스를 처리하는 ActivityResultRegistry, 요청을 실행하고 응답을 콜백처리하는 ActivityResultLauncher, 그리고 요청이 화면인지, 권한인지 또 그 응답들은 어떤타입인지에 대해 추상화된 ActivityResultContract 로 분리되어 있습니다. 그리고 추가로 이들의 통합 및 진입점 역할로써 ActivityResultCaller 가 존재합니다.
하나씩 살펴보겠습니다.
ComponentActivity 는 ActivityResultRegistryOwner 와 ActivityResultCaller 인터페이스를 구현하고 있습니다.
interface ActivityResultRegistryOwner {
val activityResultRegistry: ActivityResultRegistry
}
public interface ActivityResultCaller {
@NonNull
<I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull ActivityResultContract<I, O> contract,
@NonNull ActivityResultCallback<O> callback);
@NonNull
<I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull ActivityResultContract<I, O> contract,
@NonNull ActivityResultRegistry registry,
@NonNull ActivityResultCallback<O> callback);
}
ActivityResultRegistryOwner 는 이름그대로 ActivityResultRegistry 를 갖는 책임이 있는 인터페이스이며, ActivityResultCaller 는 ActivityResultContract 와 ActivityResultRegistry 를 가지고 요청을 실행할 ActivityResultLauncher 를 반환하고, 결과에 대한 ActivityResultCallback 을 ActivityResultRegistry 로 전달해주는 역할을 합니다. 즉, 나뉘어진 관심사들을 통합하는 역할을 합니다.
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
ActivityResultRegistryOwner,
ActivityResultCaller,
{
public ComponentActivity() {
getSavedStateRegistry().registerSavedStateProvider(ACTIVITY_RESULT_TAG,
() -> {
Bundle outState = new Bundle();
mActivityResultRegistry.onSaveInstanceState(outState);
return outState;
});
addOnContextAvailableListener(context -> {
Bundle savedInstanceState = getSavedStateRegistry()
.consumeRestoredStateForKey(ACTIVITY_RESULT_TAG);
if (savedInstanceState != null) {
mActivityResultRegistry.onRestoreInstanceState(savedInstanceState);
}
});
@NonNull
@Override
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull ActivityResultContract<I, O> contract,
@NonNull ActivityResultCallback<O> callback) {
return registerForActivityResult(contract, mActivityResultRegistry, callback);
}
@NonNull
@Override
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultRegistry registry,
@NonNull final ActivityResultCallback<O> callback) {
return registry.register(
"activity_rq#" + mNextLocalRequestCode.getAndIncrement(), this, contract, callback);
}
@NonNull
@Override
public final ActivityResultRegistry getActivityResultRegistry() {
return mActivityResultRegistry;
}
}
ComponentActivity 에서는 ActivityResultRegistry 인스턴스를 의존하며, 화면 요청 또는 권한 요청 에 대한 처리의 비즈니스는 ActivityResultRegistry 로 위임합니다. 또, 시스템에 의해 프로세스가 종료될 경우를 대비하여 ActivityResultRegistry 가 가진 상태를 저장하고 복원하고 있습니다.
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
ActivityResultRegistryOwner,
ActivityResultCaller,
{
private final ActivityResultRegistry mActivityResultRegistry = new ActivityResultRegistry() {
@SuppressWarnings("deprecation")
@Override
public <I, O> void onLaunch(
final int requestCode,
@NonNull ActivityResultContract<I, O> contract,
I input,
@Nullable ActivityOptionsCompat options) {
ComponentActivity activity = ComponentActivity.this;
// Immediate result path
final ActivityResultContract.SynchronousResult<O> synchronousResult =
contract.getSynchronousResult(activity, input);
if (synchronousResult != null) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
dispatchResult(requestCode, synchronousResult.getValue());
}
});
return;
}
// Start activity path
Intent intent = contract.createIntent(activity, input);
Bundle optionsBundle = null;
// If there are any extras, we should defensively set the classLoader
if (intent.getExtras() != null && intent.getExtras().getClassLoader() == null) {
intent.setExtrasClassLoader(activity.getClassLoader());
}
if (intent.hasExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE)) {
optionsBundle = intent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE);
intent.removeExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE);
} else if (options != null) {
optionsBundle = options.toBundle();
}
if (ACTION_REQUEST_PERMISSIONS.equals(intent.getAction())) {
// requestPermissions path
String[] permissions = intent.getStringArrayExtra(EXTRA_PERMISSIONS);
if (permissions == null) {
permissions = new String[0];
}
ActivityCompat.requestPermissions(activity, permissions, requestCode);
} else if (ACTION_INTENT_SENDER_REQUEST.equals(intent.getAction())) {
IntentSenderRequest request =
intent.getParcelableExtra(EXTRA_INTENT_SENDER_REQUEST);
try {
// startIntentSenderForResult path
ActivityCompat.startIntentSenderForResult(activity, request.getIntentSender(),
requestCode, request.getFillInIntent(), request.getFlagsMask(),
request.getFlagsValues(), 0, optionsBundle);
} catch (final IntentSender.SendIntentException e) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
dispatchResult(requestCode, RESULT_CANCELED,
new Intent().setAction(ACTION_INTENT_SENDER_REQUEST)
.putExtra(EXTRA_SEND_INTENT_EXCEPTION, e));
}
});
}
} else {
// startActivityForResult path
ActivityCompat.startActivityForResult(activity, intent, requestCode, optionsBundle);
}
}
};
@CallSuper
@Override
@Deprecated
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (!mActivityResultRegistry.dispatchResult(requestCode, resultCode, data)) {
super.onActivityResult(requestCode, resultCode, data);
}
}
@CallSuper
@Override
@Deprecated
public void onRequestPermissionsResult(
int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
if (!mActivityResultRegistry.dispatchResult(requestCode, Activity.RESULT_OK, new Intent()
.putExtra(EXTRA_PERMISSIONS, permissions)
.putExtra(EXTRA_PERMISSION_GRANT_RESULTS, grantResults))) {
if (Build.VERSION.SDK_INT >= 23) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}
}
위에서 설명한대로 내부적으로 Api 30 미만에서 호환될 수 있도록 AcitivityCompat 의 화면 또는 권한 요청 메소드들을 사용합니다. Api 30 미만에서 요청이 발생했다면, ActivityResultRegistry#dispatchResult() 를 위임하여 ActivityResultLauncher 파라미터로 넣어줬던 AcitivtyResultCallback 을 호출함으로써 결과를 람다로 실행할 수 있게 됩니다. 만약, 그 응답이 ActivityResultRegistry 로 요청한게 아니었다면 Activity#onActivityResult() 와 Activity#onRequestPermissionResult() 로 처리합니다. 간단하게 요약하면, 내부적으로 compatable 하게 동작하므로 개발자가 Api 30 미만 버전을 위해 별도의 작업을 해주지 않아도 됩니다.
public abstract class ActivityResultRegistry {
@MainThread
public abstract <I, O> void onLaunch(
int requestCode,
@NonNull ActivityResultContract<I, O> contract,
@SuppressLint("UnknownNullness") I input,
@Nullable ActivityOptionsCompat options);
public final <I, O> ActivityResultLauncher<I> register(
@NonNull final String key,
@NonNull final LifecycleOwner lifecycleOwner,
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {
Lifecycle lifecycle = lifecycleOwner.getLifecycle();
if (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
throw new IllegalStateException("LifecycleOwner " + lifecycleOwner + " is "
+ "attempting to register while current state is "
+ lifecycle.getCurrentState() + ". LifecycleOwners must call register before "
+ "they are STARTED.");
}
registerKey(key);
LifecycleContainer lifecycleContainer = mKeyToLifecycleContainers.get(key);
if (lifecycleContainer == null) {
lifecycleContainer = new LifecycleContainer(lifecycle);
}
LifecycleEventObserver observer = new LifecycleEventObserver() {
@Override
@SuppressWarnings("deprecation")
public void onStateChanged(
@NonNull LifecycleOwner lifecycleOwner,
@NonNull Lifecycle.Event event) {
if (Lifecycle.Event.ON_START.equals(event)) {
mKeyToCallback.put(key, new CallbackAndContract<>(callback, contract));
if (mParsedPendingResults.containsKey(key)) {
@SuppressWarnings("unchecked")
final O parsedPendingResult = (O) mParsedPendingResults.get(key);
mParsedPendingResults.remove(key);
callback.onActivityResult(parsedPendingResult);
}
final ActivityResult pendingResult = mPendingResults.getParcelable(key);
if (pendingResult != null) {
mPendingResults.remove(key);
callback.onActivityResult(contract.parseResult(
pendingResult.getResultCode(),
pendingResult.getData()));
}
} else if (Lifecycle.Event.ON_STOP.equals(event)) {
mKeyToCallback.remove(key);
} else if (Lifecycle.Event.ON_DESTROY.equals(event)) {
unregister(key);
}
}
};
lifecycleContainer.addObserver(observer);
mKeyToLifecycleContainers.put(key, lifecycleContainer);
return new ActivityResultLauncher<I>() {
@Override
public void launch(I input, @Nullable ActivityOptionsCompat options) {
Integer innerCode = mKeyToRc.get(key);
if (innerCode == null) {
throw new IllegalStateException("Attempting to launch an unregistered "
+ "ActivityResultLauncher with contract " + contract + " and input "
+ input + ". You must ensure the ActivityResultLauncher is registered "
+ "before calling launch().");
}
mLaunchedKeys.add(key);
try {
onLaunch(innerCode, contract, input, options);
} catch (Exception e) {
mLaunchedKeys.remove(key);
throw e;
}
}
@Override
public void unregister() {
ActivityResultRegistry.this.unregister(key);
}
@NonNull
@Override
public ActivityResultContract<I, ?> getContract() {
return contract;
}
};
}
}
ActivityResultRegistry 에서 몇가지 중요한 메소드만 가져왔습니다. onLaunch() 추상메소드는 ComponentActivity 에서 인스턴스 생성시에 구현하고 있으며, ComponentActivity#registerActivityForResult() 호출이 ActivityResultRegistry#register() 를 호출하고, 이 메소드가 반환하는 ActivityResultLauncher 의 launch 구현이 ActivityResultRegistry#onLaunch() 를 호출합니다.
결과적으로 ComponentActivity 에서 생성하는 ActivityResultRegistry 인스턴스의 onLaunch() 구현이 실행되면서 화면 또는 권한 요청이 수행됩니다. 그리고 그 결과처리는 lifecycleEventObserver 를 등록하여 Activity 의 ON_START 이벤트가 수신 될 때 실행됩니다.
OnConfigurationChangedProvider, OnTrimMemoryProvider, OnNewIntentProvider, OnMultiWindowModeChangedProvider, OnPictureInPictureModeChangedProvider
해당 Provider 인터페이스들은 내부적으로 java.util.function 에 있는 함수형 인터페이스인 Consumer 인터페이스 타입의 콜백들을 ComponentActivity 에 저장해두고 해당 인터페이스 이름에서 알 수 있듯이 특정 동작이 수행됬을 때 등록된 콜백을 실행시켜 주도록 모두 옵저버 패턴을 사용하고 있습니다.
예를들어 OnConfigurationChangedProvider 는 configuration change 가 발생했을 때 등록된 콜백들을 실행합니다.
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
OnConfigurationChangedProvider,
OnTrimMemoryProvider,
OnNewIntentProvider,
OnMultiWindowModeChangedProvider,
OnPictureInPictureModeChangedProvider,
MenuHost,
FullyDrawnReporterOwner {
private final CopyOnWriteArrayList<Consumer<Configuration>> mOnConfigurationChangedListeners = new CopyOnWriteArrayList<>();
@CallSuper
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
for (Consumer<Configuration> listener : mOnConfigurationChangedListeners) {
listener.accept(newConfig);
}
}
@Override
public final void addOnConfigurationChangedListener(
@NonNull Consumer<Configuration> listener
) {
mOnConfigurationChangedListeners.add(listener);
}
@Override
public final void removeOnConfigurationChangedListener(
@NonNull Consumer<Configuration> listener
) {
mOnConfigurationChangedListeners.remove(listener);
}
다른 Provider 들도 마찬가지로 CopyOnWriteArrayList 클래스를 이용합니다. 해당 클래스는 위에서 한번 설명했듯이 멀티 스레드 환경에서 다른 스레드에서 쓰기작업이 일어나는 동시에 해당 데이터에 순회하고 있어도 읽기 작업에는 복사본(스냅샷)을 이용하기 때문에 ConcurrentModificationException 이 발생하지 않도록 안전하게 동작시킬 수 있는 클래스입니다. 다시 한번 언급드리지만 해당 클래스는 쓰기 작업에는 synchronized 블록에서 배열을 복사하여 속도가 느려지기 때문에 쓰기 작업이 적고 읽기가 빈번한 경우 적합합니다. 만약 쓰기가 빈번하다면 SynchronizedList 가 더 나은 선택일 수 있습니다.
OnTrimMemoryProvider 는 안드로이드 시스템에 의해 프로세스의 메모리가 정리될 때 실행할 콜백들을 다루는 인터페이스이고, OnNewIntentProvider 는 Activity 살펴보기 : Activity 의 이해 에서 다뤘던 내용으로 ComponentActivity#onNewIntent() 가 호출될 때 실행할 콜백들을 다루는 인터페이스 입니다. OnMultiWindowModeChangedProvider 는 Activity 가 멀티윈도우 모드로 전환될 때 실행할 콜백들을 다루는 인터페이스 이며, 마지막으로 OnPictureInPictureModeChangedProvider 는 Activity 가 PiP(Picture-in-picture) 모드로 전환될 때 실행할 콜백들을 다루는 인터페이스입니다. 모두 동일한 형태로 작성되었기 때문에 내부코드는 생략하겠습니다.
MenuHost
필요에 따라 Activity 내에서 상단바를 추가하거나 AppCompatActivity 에서 지원하는 theme 에 정의된 actionBar 에 메뉴들을 삽입 또는 제거 및 handling 할 필요가 있습니다. 그럴 때 필요한 것이 ComponentActivity 에 있는 MenuHost 인터페이스 입니다.
메뉴를 다루기 위해 필요한 MenuProvider 는 상단바에 메뉴를 생성하거나 선택되었을 때의 콜백처리와 관련된 기능을 제공하는 인터페이스입니다. 그리고 MenuHost 는 상단바에 메뉴를 등록하고, 다루기 위한 MenuProvider 를 제어 및 관리하는 인터페이스 입니다.
public interface MenuHost {
void addMenuProvider(@NonNull MenuProvider var1);
void addMenuProvider(@NonNull MenuProvider var1, @NonNull LifecycleOwner var2);
@SuppressLint({"LambdaLast"})
void addMenuProvider(@NonNull MenuProvider var1, @NonNull LifecycleOwner var2, @NonNull Lifecycle.State var3);
void removeMenuProvider(@NonNull MenuProvider var1);
void invalidateMenu();
}
public interface MenuProvider {
default void onPrepareMenu(@NonNull Menu menu) {}
void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater);
boolean onMenuItemSelected(@NonNull MenuItem menuItem);
default void onMenuClosed(@NonNull Menu menu) {}
}
ComponentActivity 는 MenuHost 인터페이스를 구현하여 MenuProvider 를 handling 할 수 있는 기능을 가지며, 내부적으로 MenuHostHelper 인스턴스로 lifecycle-aware 하게 동작시키고 여러 MenuProvider 들을 관리합니다.
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
MenuHost {
private final MenuHostHelper mMenuHostHelper = new MenuHostHelper(this::invalidateMenu)
@Override
public void addMenuProvider(@NonNull MenuProvider provider, @NonNull LifecycleOwner owner) {
mMenuHostHelper.addMenuProvider(provider, owner);
}
@Override
@SuppressLint("LambdaLast")
public void addMenuProvider(@NonNull MenuProvider provider, @NonNull LifecycleOwner owner, @NonNull Lifecycle.State state) {
mMenuHostHelper.addMenuProvider(provider, owner, state);
}
@Override
public void removeMenuProvider(@NonNull MenuProvider provider) {
mMenuHostHelper.removeMenuProvider(provider);
}
}
public class MenuHostHelper {
private final Runnable mOnInvalidateMenuCallback;
private final CopyOnWriteArrayList<MenuProvider> mMenuProviders = new CopyOnWriteArrayList();
private final Map<MenuProvider, LifecycleContainer> mProviderToLifecycleContainers = new HashMap();
public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
Iterator var3 = this.mMenuProviders.iterator();
while(var3.hasNext()) {
MenuProvider menuProvider = (MenuProvider)var3.next();
menuProvider.onCreateMenu(menu, menuInflater);
}
}
public boolean onMenuItemSelected(@NonNull MenuItem item) {
Iterator var2 = this.mMenuProviders.iterator();
MenuProvider menuProvider;
do {
if (!var2.hasNext()) {
return false;
}
menuProvider = (MenuProvider)var2.next();
} while(!menuProvider.onMenuItemSelected(item));
return true;
}
public void addMenuProvider(@NonNull MenuProvider provider, @NonNull LifecycleOwner owner) {
this.addMenuProvider(provider);
Lifecycle lifecycle = owner.getLifecycle();
LifecycleContainer lifecycleContainer = (LifecycleContainer)this.mProviderToLifecycleContainers.remove(provider);
if (lifecycleContainer != null) {
lifecycleContainer.clearObservers();
}
LifecycleEventObserver observer = (source, event) -> {
if (event == Event.ON_DESTROY) {
this.removeMenuProvider(provider);
}
};
this.mProviderToLifecycleContainers.put(provider, new LifecycleContainer(lifecycle, observer));
}
@SuppressLint({"LambdaLast"})
public void addMenuProvider(@NonNull MenuProvider provider, @NonNull LifecycleOwner owner, @NonNull Lifecycle.State state) {
Lifecycle lifecycle = owner.getLifecycle();
LifecycleContainer lifecycleContainer = (LifecycleContainer)this.mProviderToLifecycleContainers.remove(provider);
if (lifecycleContainer != null) {
lifecycleContainer.clearObservers();
}
LifecycleEventObserver observer = (source, event) -> {
if (event == Event.upTo(state)) {
this.addMenuProvider(provider);
} else if (event == Event.ON_DESTROY) {
this.removeMenuProvider(provider);
} else if (event == Event.downFrom(state)) {
this.mMenuProviders.remove(provider);
this.mOnInvalidateMenuCallback.run();
}
};
this.mProviderToLifecycleContainers.put(provider, new LifecycleContainer(lifecycle, observer));
}
public void removeMenuProvider(@NonNull MenuProvider provider) {
this.mMenuProviders.remove(provider);
LifecycleContainer lifecycleContainer = (LifecycleContainer)this.mProviderToLifecycleContainers.remove(provider);
if (lifecycleContainer != null) {
lifecycleContainer.clearObservers();
}
this.mOnInvalidateMenuCallback.run();
}
실제 사용은 ComponentActivity#onCreate() 콜백에서 MenuProvider 인스턴스를 생성하여 ComponentActivity#addMenuProvider() 로 전달하면 됩니다.
class ExampleActivity : ComponentActivity {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
addMenuProvider(object : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.example_menu, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return true
}
})
}
}
FullyDrawnReporterOwner
개발자가 만든 앱은 다양한 요인에 의해 런쳐앱 을 통해 앱을 실행하고 사용자와 상호작용을 하기 위해 준비되는 시간이 모두 다를 수 있습니다. 인스타그램 앱은 피드 화면에 모든 게시물들이 표시되어야 사용자와 상호작용이 가능한 시점일 수도 있고, 카카오톡의 대화방은 상대방의 최근 수신된 메세지가 표시된 시점일 수도 있습니다. 이 시간(TTFD)이 너무 느려질 경우 사용자 경험은 현저히 떨어지게 됩니다. 즉, 낮은 TTFD 시간은 높은 사용자 경험을 제공할 수 있습니다. 안드로이드에서는 이 병목현상에 대한 시간을 세부적으로 측정하고 해결할 수 있도록 여러가지 방법들을 가이드하고 있습니다.
ComponentActivity#reportFullyDrawn() 가 이 때 사용하는 메소드입니다. 이 메소드는 TTFD(앱 실행 인텐트를 시스템이 수신한 순간부터 Activity#reportFullyDrawn() 가 호출될 때 까지 걸린 시간)를 측정하기 위한 용도로써 제공됩니다. 이 메소드는 이름 그대로, 화면이 완전히 보여지고 사용자와 상호작용 할 수 있는 시점에 호출한 경우 시스템에 알리게되고, 개발자는 Log 를 통해 그 때 까지의 시간을 확인할 수 있습니다. ComponentActivity#reportFullyDrawn() 는 직접 호출 할 수도 있지만, FullyDrawnReporter 를 통해 호출하는 것이 권장되는 방법입니다. FullyDrawnReporter 는 앱 프로세스 생성부터 첫 프레임이 그려질 때 까지 걸리는 시간(TTID) 이후 외부 Api 로 이미지를 로드하여 화면내의 이미지뷰에 표시하는 것과 같이 시간이 걸리는 타이밍을 제어하거나 완료된 타이밍에 특정 작업을 수행할 콜백을 등록하는 등 다양한 기능들을 제공하고 있습니다.
FullyDrawnReporterOwner 인터페이스는 FullyDrawnReporter 를 반환하는 함수형 인터페이스입니다. ComponentActivity 는 이 인터페이스를 구현하여 FullyDrawnReporter 를 갖게 됩니다.
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
FullyDrawnReporterOwner {
final ReportFullyDrawnExecutor mReportFullyDrawnExecutor = createFullyDrawnExecutor();
@NonNull
final FullyDrawnReporter mFullyDrawnReporter = new FullyDrawnReporter( mReportFullyDrawnExecutor, () -> { reportFullyDrawn(); return null;});
@NonNull
@Override
public FullyDrawnReporter getFullyDrawnReporter() {
return mFullyDrawnReporter;
}
@Override
public void reportFullyDrawn() {
try {
if (Trace.isEnabled()) {
// TODO: Ideally we'd include getComponentName() (as later versions of platform
// do), but b/175345114 needs to be addressed.
Trace.beginSection("reportFullyDrawn() for ComponentActivity");
}
if (Build.VERSION.SDK_INT > 19) {
super.reportFullyDrawn();
} else if (Build.VERSION.SDK_INT == 19 && ContextCompat.checkSelfPermission(this,
Manifest.permission.UPDATE_DEVICE_STATS) == PackageManager.PERMISSION_GRANTED) {
// On API 19, the Activity.reportFullyDrawn() method requires the
// UPDATE_DEVICE_STATS permission, otherwise it throws an exception. Instead of
// throwing, we fall back to a no-op call.
super.reportFullyDrawn();
}
// Activity.reportFullyDrawn() was added in API 19, so we can't call super
// prior to that, but we still need to update our FullyLoadedReporter's state
mFullyDrawnReporter.fullyDrawnReported();
} finally {
Trace.endSection();
}
}
FullyDrawnReporter#addReporter() 가 호출되면 제거되기 전까지 ComponentActivity#reportFullyDrawn() 이 호출되지 않도록 lock 하고, FullyDrawnReporter#removeReporter() 가 호출되면 내부적으로 ComponentActivity#reportFullyDrawn() 을 호출함으로써 시스템에 사용자와 상호작용 할 수 있는 타이밍임을 알리며 이 타이밍에 실행해줄 등록된 콜백들을 실행해줍니다.
class FullyDrawnReporter(
private val executor: Executor,
private val reportFullyDrawn: () -> Unit
) {
private val lock = Any()
@GuardedBy("lock")
private var reporterCount = 0
@GuardedBy("lock")
private var reportPosted = false
@GuardedBy("lock")
private var reportedFullyDrawn = false
/**
* Returns `true` after [reportFullyDrawn] has been called or if backed by a
* [ComponentActivity] and [ComponentActivity.reportFullyDrawn] has been called.
*/
val isFullyDrawnReported: Boolean
get() {
return synchronized(lock) { reportedFullyDrawn }
}
@GuardedBy("lock")
private val onReportCallbacks = mutableListOf<() -> Unit>()
private val reportRunnable: Runnable = Runnable {
synchronized(lock) {
reportPosted = false
if (reporterCount == 0 && !reportedFullyDrawn) {
reportFullyDrawn()
fullyDrawnReported()
}
}
}
/**
* Adds a lock to prevent calling [reportFullyDrawn].
*/
fun addReporter() {
synchronized(lock) {
if (!reportedFullyDrawn) {
reporterCount++
}
}
}
/**
* Removes a lock added in [addReporter]. When all locks have been removed,
* [reportFullyDrawn] will be called on the next animation frame.
*/
fun removeReporter() {
synchronized(lock) {
if (!reportedFullyDrawn && reporterCount > 0) {
reporterCount--
postWhenReportersAreDone()
}
}
}
/**
* Registers [callback] to be called when [reportFullyDrawn] is called by this class.
* If it has already been called, then [callback] will be called immediately.
*
* Once [callback] has been called, it will be removed and [removeOnReportDrawnListener]
* does not need to be called to remove it.
*/
fun addOnReportDrawnListener(callback: () -> Unit) {
val callImmediately =
synchronized(lock) {
if (reportedFullyDrawn) {
true
} else {
onReportCallbacks += callback
false
}
}
if (callImmediately) {
callback()
}
}
/**
* Removes a previously registered [callback] so that it won't be called when
* [reportFullyDrawn] is called by this class.
*/
fun removeOnReportDrawnListener(callback: () -> Unit) {
synchronized(lock) {
onReportCallbacks -= callback
}
}
/**
* Must be called when when [reportFullyDrawn] is called to indicate that
* [Activity.reportFullyDrawn] has been called. This method should also be called
* if [Activity.reportFullyDrawn] has been called outside of this class.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun fullyDrawnReported() {
synchronized(lock) {
reportedFullyDrawn = true
onReportCallbacks.forEach { it() }
onReportCallbacks.clear()
}
}
/**
* Posts a request to report that the Activity is fully drawn on the next animation frame.
* On the next animation frame, it will check again that there are no other reporters
* that have yet to complete.
*/
private fun postWhenReportersAreDone() {
if (!reportPosted && reporterCount == 0) {
reportPosted = true
executor.execute(reportRunnable)
}
}
}
사용방법은 두가지가 존재합니다.
val fullyDrawnReporter = componentActivity.fullyDrawnReporter
launch {
fullyDrawnReporter.reportWhenComplete {
dataLoadedMutex.lock()
dataLoadedMutex.unlock()
}
}
해당 방법은 코루틴을 이용하는 방법으로 해당 람다 파라미터는 suspend 이며, 람다 내부에 외부 Api 로 부터 이미지를 가져오고 로드하는 등의 시간이 걸리는 작업을 두어 실제로 사용자와 상호작용 할 수 있는 타이밍을 정확하게 알 수 있습니다.
만약 코루틴을 이용하지 않는다면,
// On the UI thread
fullyDrawnReporter.addReporter()
// Do the loading on worker thread
fullyDrawnReporter.removeReporter()
// On the UI thread
FullyDrawnReporter#addReporter() 와 FullyDrawnReporter#removeReporter() 로 직접 제어할 수 있습니다. 내부적으로는 synchronized 를 이용하여 lock 과 unlock 이후 ComponentActivity#reportFullyDrawn() 을 호출하게 됩니다.
Compose 의 경우 androidx.activity.compose.ReportDrawn.kt 에 있는 함수들을 이용합니다. CompositionLocals 로 LocalFullyDrawnReporterOwner 가 provide 되고 있으며, ReportDrawnAfter(suspend () -> Unit) 로 코루틴을 이용하는 방법으로 사용할 수 있고 ReportDrawn() 로 Composition 이 완료되는 타이밍에 ComponentActivity#reportFullyDrawn() 를 호출할 수 도 있습니다.
끝으로
이렇게 해서 ComponentActivity 에 대해서 다루어 보았습니다. ComponentActivity 는 생각보다 많은 일들을 처리해줄 수 있고, 안드로이드는 사용자 경험을 위해 다양한 기능들을 제공하고 있습니다. 이러한 지식은 알고 있다면 필요한 상황속에서 빠르게 캐치하여 적용할 수 있을 것 입니다. 내부 코드들은 지속적으로 바뀌기 때문에 디테일 한 내용들 까지 알 필요는 없습니다. 단지, 어떤 자료구조를 이용했고 그 이유는 뭐였을지를 생각하면서 자료구조의 이해도를 높이고 어떤 기능들이 있는지 미리 알아두는 과정으로 Activity 에 대한 이해를 더 넓혀갈 수 있기 때문에 유의미한 경험이라고 생각합니다.
댓글남기기