devlog of ShinJe Kim

[Android] 프래그먼트에 기본생성자가 꼭 있어야하는 이유

|

프래그먼트에 기본생성자가 꼭 있어야하는 이유

All subclasses of Fragment must include a public no-argument constructor. The framework will often re-instantiate a fragment class when needed, in particular during state restore, and needs to be able to find this constructor to instantiate it. If the no-argument constructor is not available, a runtime exception will occur in some cases during state restore.

프래그먼트의 모든 서브클래스는 인자가 없는 생성자를 가져야만 합니다. 프레임워크는 종종 필요할 때 프래그먼트 클래스를 재인스턴스화하는데, 특히 상태(state)를 복구할 때 그렇습니다. 이 때 프래그먼트 클래스를 인스턴스화 하기 위해서는 빈 생성자가 필요합니다. 만약 빈 생성자를 사용할 수 없으면 상태 복구 도중에 런타임 익셉션이 발생할 수도 있습니다.

안드로이드 공식문서 - Fragment

즉, 안드로이드가 프래그먼트를 복원할 때 빈 생성자가 필요하고, 빈 생성자가 없으면 안드로이드 시스템이 프래그먼트를 생성할 수 없어 에러가 난다는 것이다.

(+빈 생성자가 없으면 안드로이드 시스템이 왜 프래그먼트를 생성할 수 없는지? 정리할 것)

[Android] 액티비티, 프래그먼트 라이프사이클 정리

|

액티비티, 프래그먼트 라이프사이클 정리

액티비티와 프래그먼트의 라이프사이클을 총체적으로 정리해보았다.

액티비티의 모든 인스턴스는 라이프사이클을 가지는데, 이 라이프사이클 동안 액티비티는 실행(running), 일시 중지(paused), 중단(stopped)의 3가지 상태를 가진다. 각 전환이 발생할 때 액티비티에 상태 변경을 알려주는 액티비티 메서드들이 있으며 이 메서드들을 안드로이드 런타임이 자동 호출한다.

‘실무에 바로 적용하는 안드로이드 프로그래밍’, 빌 필립스

액티비티 라이프사이클은 다음과 같은 순서로 진행된다.

onCreate() -> onStart() -> onResume() -> onPause() -> onStop() -> onDestroy()

(onRestart()는 onStop() 이후에 호출되며, 다시 onStart()를 호출한다)

그렇다면 라이프 사이클이 진행되는 동안 액티비티의 상태는 어떻게 될까? 우선 액티비티가 존재하지 않을 때부터 실행(running)될 때 까지의 순서로 살펴보자. onCreate()가 호출되기 이전 앱의 액티비티는 존재하지 않는다.

만약 onCreate()가 호출되면 액티비티는 중단(stopped)상태로 전환된다. 아직까지는 사용자가 화면을 볼 수 없다. 다음으로 onStart()가 호출되면 액티비티가 일시중지(paused) 상태로 전환되어 화면이 보이게 되며, onResume()이 호출되고 나서야 비로소 액티비티가 실행(running) 상태가 된다. 이 때의 액티비티는 포그라운드에 존재한다.

이제는 실행중인 액티비티가 소멸될 때까지의 과정을 살펴보자.

소멸 과정은 생성에서 실행되었던 과정을 반대로 거슬러 올라간다고 보면 된다. 실행(running)중인 액티비티에서 onPause()가 호출되면 액티비티는 일시 중지(pause)상태가 되며 아직 화면은 보이는 상태이다.

여기서 onStop()이 호출되면 액티비티는 중단(stopped) 상태로 전한되며 화면에 나타나지 않게 된다. 하지만 화면에 보이지 않는다고 해서 액티비티가 존재하지 않는 것은 아니다. 마지막으로 onDestroy()까지 호출되어야 비로소 액티비티가 소멸되어 더 이상 존재하지 않게 된다.

위에서 언급한 메서드들은 개발자가 직접 호출하여 실행하는 것이 아니라 안드로이드 시스템이 실행하는 것이며, 개발자는 하고자 하는 동작을 적절한 메서드에서 오버라이딩하여 작성해야 한다. 하지만 나같은 초보는 막상 코드를 작성하려고 하면 머릿속이 하얘지고 이런 생각이 든다.

“라이프사이클이 저런 구조를 가진다는 것은 알겠는데, 그래서 내가 작성하고자 하는 코드를 대체 어디 넣어야 하는거지?”

이럴 때는 백문이 불여일타. 샘플앱을 만들어 각각의 메서드에 로그를 찍어보고 이것저것 동작시켜보았다. (사실 이 글은 앱을 제대로 만들지 못해서 3주째 삽질을 하다가 정리해보는 글이다. 왜 진작에 이렇게 정리해 볼 생각을 못했는지 도무지 이해가 안가는 과거의 나.. 반성하자)

야래와 같이 액티비티를 하나 만들고 위의 6개 메서드에 각각 로그를 찍어보았다(잘 보이게 하기 위해 에러 로그를 사용하였다). 앱을 실행함과 동시에 onCreate(), onStart(), onResume()이 호출된다.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.e("TAG ", "onCreate(), MainActivity")
    }

    override fun onStart() {
        super.onStart()
        Log.e("TAG ", "onStart(), MainActivity")
    }

    override fun onResume() {
        super.onResume()
        Log.e("TAG ", "onResume(), MainActivity")
    }

    override fun onPause() {
        super.onPause()
        Log.e("TAG ", "onPause(), MainActivity")
    }

    override fun onStop() {
        super.onStop()
        Log.e("TAG ", "onStop(), MainActivity")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.e("TAG ", "onDestroy(), MainActivity")
    }
}

이 때 홈버튼을 클릭하여 홈 화면으로 나가면(앱을 종료하는 것은 아님) onPause()와 onStop()이 호출된다. 그리고 다시 앱을 띄우면 onStart()와 onResume()이 호출된다. 오버뷰(overview) 버튼 (네모 버튼) 을 클릭해도 마찬가지로 onPause()와 onStop()이 호출되고, 다시 앱을 띄울 때 onStart()와 onResume()이 호출된다.

그럼 만약 앱이 화면에 띄워진 상태에서 백버튼을 누르면 어떻게 될까? onPause(), onStop()과 더불어 onDestroy()까지 호출된다. 액티비티가 소멸되었기때문에 다시 앱으로 돌아갔을때에는 onCreate()부터 onStart(), onResume()까지 호출된다.

이제 액티비티 라이프사이클에 대해서 확실히 알아보았으니 프래그먼트의 라이프사이클로 넘어가보자.

프래그먼트 라이프사이클은 다음과 같은 순서로 진행된다.

onAttach() -> onCreate() -> onCreateView() -> onActivityCreated() -> onStart() -> onResume() -> onPause() -> onStop() -> onDestroyView() -> onDestroy() -> onDetach()

위에서 로그를 찍어보았던 메인액티비티에 FrameLayout을 만들고 그 안에 아래와 같이 프래그먼트를 add해보았다. 그리고 프래그먼트에도 위의 액티비티에서 로그를 찍었던 것처럼 모든 오버라이딩 메소드에 로그를 찍어보았다.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.e("TAG ", "onCreate(), MainActivity")

        supportFragmentManager.beginTransaction()
            .add(R.id.frameContainer, FirstFragment())
            .commit()
    }

앱을 실행하자마자 위에서 보았던것처럼 액티비티의 onCreate()가 호출되었고, 이어서 프래그먼트의 onAttach(), onCreate(), onCreateView(), onViewCreated(), onActivityCreated()가 차례대로 호출되었다. 그리고 액티비티의 onStart()와 onResume()이 호출되고 난 후에 프래그먼트의 onResume()이 호출되었다. 액티비티를 생성한 다음 프래그먼트를 생성하고, 액티비티를 포그라운드로 보낸 다음(실행 상태) 프래그먼트를 포그라운드로 보낸 것이다.

백버튼을 눌러보았더니 예상 밖의 순서로 진행되었다. 프래그먼트가 먼저 onPause()를 호출하고 액티비티의 onPause()가 호출되었다. 그리고 프래그먼트의 onStop()이 먼저 호출된 후 액티비티의 onStop()이 호출되었으며, 이어서 프래그먼트의 onDestroyView(), onDestroy(), onDetach()가 호출된 후 액티비티의 onDestroy()가 호출되었다. 앱을 다시 화면에 띄우니 처음 실행할때의 순서대로 똑같은 메서드들이 호출되었다.

취소 버튼을 눌렀을때에는 프래그먼트 onPause() -> 액티비티 onPause() -> 프래그먼트 onStop() -> 액티비티 onStop() 순서로 호출되었다. 그리고 다시 앱을 화면에 띄우면 액티비티 onStart() -> 액티비티 onResume() -> 프래그먼트 onResume() 순서로 호출되었다. 오버뷰 버튼을 눌렀을때 역시 마찬가지였다.

이렇게 한 번 보고 나니 액티비티와 프래그먼트가 각기 어떤 흐름으로 실행되는 것인지 감이 온다. 이제 실전에서 적용해 볼 차례이다.

참고 자료

[TIL] 2019-11-28

|

Today I Learned

조건이 여러개일 때 조건문을 깔끔하게 작성하기가 어렵다. 두 가지 조건이 있다.

  1. item의 상태가 selected일 때(isSelected = true) textView의 색상은 파란색, 반대의 경우에는 빨간색.
  2. 빌드 버전에 따라 사용할 수 있는 getColor api가 다르다. 23레벨 이상의 getColor에는 color값과 더불어 theme값을 넣어주어야 한다.

내가 작성한 코드. 구구절절 조건을 나열했고 필요없는 반복이 많다.(구질구질..) 어떻게든 수정해보려고 했는데 잘 안되었다.

if(isSelected) {
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        textView.setTextColor(resources.getColor(R.color.blue, null))
    } else {
        textView.setTextColor(resources.getColor(R.color.blue))
    }
} else {
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        textView.setTextColor(resources.getColor(R.color.red, null))
    } else {
        textView.setTextColor(resources.getColor(R.color.red))
    }
}

CTO님의 손길을 통해 개선된 코드

val getColor = { resourceId: Int ->
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES>M) {
        resources.getColor(resourceId, null)
    } else {
        resources.getColor(resourceId)
    }
}

tagTextView.setTextColor(getColor(if (isSelected)
    R.color.fixed_text_title
else
    R.color.text_title
))

최대한 중복되는 부분을 줄이고 바뀌는 부분만 변화하는 형태로 코드를 짜도록 하자.

[TIL] 2019-11-25

|

Today I Learned

  • Dialog를 만들기 위해 찾아보니 종류가 많다. 안드로이드 공식문서의 대화상자부분을 보니 아래와 같은 방법이 있다.
    1. AlertDialog: 제목 하나, 최대 세 개의 버튼, 선택 가능한 품목 목록 또는 사용자 지정 레이아웃을 표시할 수 있는 대화상자
    2. DatePickerDialog 또는 TimePickerDialog: 미리 정의된 UI가 있는 대화상자이며, 사용자가 날짜 또는 시간을 선택할 수 있음
    3. DialogFragment: 대화상자를 만드록 외형을 관리하는 데 필요한 모든 컨트롤을 제공함. 사용자가 Back 버튼을 누르거나 화면을 돌리는 등과 같은 수명 주기 이벤트를 올바르게 처리할 수 있음.
  • 여기서 기억해야 할 것은, AlertDialog로 구현하면 PositiveButton/NegativeButton을 누르면 무조건 다이얼로그 창이 닫힌다는 것이다.
  • 나는 다이얼로그의 textEdit을 검사하여 사용자가 “확인” 버튼을 클릭했을 때 Toast를 띄워주고자 했었는데, 이것도 모르고 한참을 AlertDialog로 삽질했다.
  • 커스텀 다이얼로그를 만들기 위해서는 DialogFragment를 구현해야 한다. 내가 구현하고자 했던것처럼 버튼을 클릭해도 창이 닫히지 않는 다이얼로그를 원한다면 AlertDialog로 생성하는 것이 아니라 DialogFragment를 상속받고, 그냥 일반 fragment처럼 onCreateView에서 뷰를 inflate 해주면 된다.

[TIL] 2019-11-21

|

Today I Learned

코드를 작성할 떄 변하는 부분과 변하지 않는 부분을 분리하자.

// 변경 전
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
    if (commentEditText.text.isBlank())
        commentTextHint.visibility = View.VISIBLE
    else
        commentTextHint.visibility = View.GONE
}
// 변경 후
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
    commentTextHint.visibility = if (commentEditText.text.isBlank())
        View.VISIBLE
    else
        View.GONE
}