Activity Controller
- 강사가 소개하는 fragment 관리 방법
- 웹, 모바일 등 애플리케이션에서 눈에 보이는 화면을 관리하는 요소를 controller 라고 함 (MVC 패턴)
- 눈에 보이는 모든 부분을 fragment 로 만들어 사용할 경우 , Fragment를 관리하는 Activity가 Controller 역할을 함 -> 어떤 프래그먼트를 보여줄지에 대한 코드를 액티비티에서 작성하는 경우
- 액티비티를 통해 보여지는 프래그먼트들은 자신을 관리하는 액티비티에 접근이 가능
Activity의 역할
- 각 Fragment를 교환, 관리
- Fragment들이 사용하는 데이터를 관리
[MainActivity.kt] -> 프래그먼트를 관리
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
val frag1 = InputFragment()
val frag2 = ResultFragment()
// 프래그먼트들이 사용한 변수
var value1 = ""
var value2 = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) // 뷰바인딩 객체
val view = binding.root //뷰바인딩을 통해 레이아웃과 뷰가 결합 -> .root 를 통해 View 객체만를 뽑아내는(?)
setContentView(view)
setFragment("input") //기본 설정은 input 프래그먼트
}
fun setFragment(name : String) {
val tran = supportFragmentManager.beginTransaction()
// 분기 (유지보수에 용이)
when (name) {
"input" -> {
tran.replace(R.id.container1, frag1)
}
"result" -> {
tran.replace(R.id.container1, frag2)
tran.addToBackStack(null) // 결과 보고, 뒤로가기 누르면 다시 입력할 수 있는 프래그먼트로
}
}
tran.commit()
}
}
[InputFragment.kt]
class InputFragment : Fragment() {
private lateinit var frag_binding : FragmentInputBinding
// 프래그먼트가 액티비티와 연결될 떄 호출
override fun onAttach(context: Context) {
super.onAttach(context)
Log.d("test","onAttach")
}
// 프래그먼트가 생성될 때 호출
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("test","onCreate")
}
// 프래그먼트를 통해 보여줄 view 객체를 생성하기 '위해' 사용
// 프래그먼트 내 뷰를 제어하기 위해서는 여기서 필요한 처리를 함
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Log.d("test","onCreateView")
frag_binding = FragmentInputBinding.inflate(layoutInflater) // 뷰바인딩 객체
val view = frag_binding.root
// 결과 화면에서 입력 화면으로 뒤로가기 하면 이전에 입력했던 내용 사라짐
//Log.d("test",frag_binding.inputEdit1.text.toString()) // null
frag_binding.inputEdit1.setText("sss") // 이 코드를 이 위치에 넣으면 동작은 하는데 화면에 반영이 안 됨, onStart/onResume 에 넣으면 됨
//frag_binding.inputEdit2.setText("")
//Log.d("test",frag_binding.inputEdit1.text.toString()) // sss
/*
* Back Stack에서 이전 프래그먼트를 꺼내 돌아오면서 현재 UI Thread가 input 프래그먼트로 아직 바뀌지 않은 상태이기 때문
* Log를 찍어보면 inputEdit1이 null -> sss 로 바뀌는 것을 확인할 수 있다. 새로 생성한 binding view 객체에 문자열 셋팅은
* 정상적으로 되지만 그것을 화면에 표시해주는 것이 느릴뿐....
* 방법1.binding도 새로 해주고, findViewById를 사용해 EditText를 다시 찾아줌 (난 이 방법도 안됨)
* 방법2.스레드를 사용해 잠깐 sleep 을 주고 setText() 호출 ->
* 방법3.onStart()/onResume()에서 수행
* 따라서 나의 경우 방법1은 배제하고 방법2/방법3을 사용하였다. 방법의 2의 경우 백그라운드 스레드를 만들어
* 나도 처음에는 그럼 그냥 main thread 에서 sleep 했다가 setText 호출하면 되는거 아니야?
* 라고 생각했지만....sleep을 하는 것도 그 시간동안 main thread가 아무것도 안 하게 되는거다.
* 즉, ui thread 전환중... -> sleep 하느라 올 스탑 -> setText 수행 (ui thread는 아직 전환중이라구욧...!!;;)
* 이런 상황이 되는 것이다. 그래서 ui thread 와 sleep 을 해주는 스레드를 분리해야 하고
* 사용자 백그라운드 스레드를 만들어 ui thread 가 충분히 현재 화면으로 바뀌는동안 기다렸다가 setText를 수행해줘야 한다.
* */
/*
// 현재 ui 스레드가 내가 원하는 새로 binding한 스레드가 아닌 문제?
// 새로운 스레드 만들어서 약간의 delay를 주고 setText() 작업을 수행하면 현재 ui thread가 뭐인지에 상관없이 해당 작업 수행 가능
val thread1 = object : Thread() {
override fun run() {
super.run()
// 개발자가 발생시킨 스레드에서 오래 걸리는 작업을 수행
SystemClock.sleep(7) // 짧게 주면 사용자 입장에서는 거의 바로 바꾸는 것으로 보임 (10초도 살짝 잔상 보임)
frag_binding.inputEdit1.setText("")
frag_binding.inputEdit2.setText("")
}
}
thread1.start()
*/
// 뷰바인딩을 통해 뷰 객체를 정의하고 기능을 구현할 수 있다.
frag_binding.button.setOnClickListener {
// input 값을 보내 MainActivity 에서 결과 프래그먼트 화면을 호출하도록 함
val mainActivity = activity as MainActivity // 여기서 activity는 FragmentActivity 라는 타입 -> AppCompatActivity 타입으로 형변환
mainActivity.value1 = frag_binding.inputEdit1.text.toString()
mainActivity.value2 = frag_binding.inputEdit2.text.toString()
mainActivity.setFragment("result")
}
return view
}
[ResultFragment.kt]
class ResultFragment : Fragment() {
private lateinit var frag_binding : FragmentResultBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
frag_binding = FragmentResultBinding.inflate(layoutInflater)
val view = frag_binding.root
val mainActivity = activity as MainActivity
frag_binding.resultText1.text = mainActivity.value1
frag_binding.resultText2.text = mainActivity.value2
return view
}
}
💡 프래그먼트에서 이전 화면으로 돌아오면 setText가 먹지 않는다?!
Input프래그먼트에서 EditTextView에 문자열을 입력하고 버튼을 눌러 Result 프래그먼트에서 결과를 확인하고 다시 Input프래그먼트로 돌아오면 EditTextView에 이전에 입력했던 값이 남아 setText() 메서드를 사용해 "" 로 설정해주는 코드를 onCreateView() 에 작성하였으나 먹히질 않았다.
=> onStart() 나 onResume()에서는 정상적으로 동작하는 것을 확인할 수 있었고, setText() 수행 이전과 이후에 log를 찍어 edittextview 변수를 확인해보니 정상적으로 변경된 것을 확인할 수 있었다. 즉, 화면에 반영만 안 되었다는 의미다.
다행히 해당 이슈는 흔히 발생하는 문제로 이전 프래그먼트로 돌아왔을 때 화면을 처리하는 UI Thread가 아직 이전 프래그먼트로 바뀌질 않았는데 (바뀔 시간이 충분하지 않아서) 내가 setText() 호출해버려서 화면에 반영이 안 된것이라고 한다.
그래서 야심차게 얼마전에 배웠던 스레드를 사용해보았다. 개발자가 만든 스레드에서 sleep을 잠깐 해주고 setText 를 호출하는 것이다. 그러면 sleep 하는 시간동안 화면이 다시 바뀌고 setText가 반영이 되는 것이다. 개발자가 만든 스레드와 Main Thread에서 작동하는 UI Thread는 별개로 동작하기 때문에 가능한일이다. 무심코 "기다려주는게 필요한거면 그냥 sleep() 해주면 되는거 아닐까?" 라고 생각할 수 있다. 하지만 Main(UI) Thread에서 sleep() 해주는 것은 그냥 Main(UI) Thread가 그동안 아무것도 안 하는 것이다. 즉, 화면이 바뀌는 시간이 생기는게 아니라 그냥 아무것도 안 하는거고 sleep 에서 풀려나면 화면이 바뀌지 않은 그 상태 그대로 setText가 호출되는 것이다.
개발자 스레드에서 sleep()은 너무 길게 주면 문자열이 바뀌는게 눈에 보이기 때문에 10ms 미만으로 설정해주는게 좋다. (알아서)
여기서는 MVC 패턴을 사용했지만 요즘 안드로이드 대세 패턴/아키텍처는 MVVM인 것 같다.
빠른 시일 내에 MVVM 을 공부하고 프로젝트에 적용해야징 👍🏻
'Android' 카테고리의 다른 글
[안드로이드/Fragment] DialogFragment (0) | 2023.04.22 |
---|---|
[안드로이드/Fragment] ListFragment (0) | 2023.04.22 |
[안드로이드/Fragment] Fragment 내 View 제어 (0) | 2023.04.21 |
[안드로이드/Fragment] Fragment 생명주기 (1) | 2023.04.21 |
[안드로이드/Fragment] Fragment 란? (0) | 2023.04.21 |