Learn & Run

실무에서 적용한 Activity 또는 Fragment간 통신 방법 with ViewModel, LiveData 본문

Android

실무에서 적용한 Activity 또는 Fragment간 통신 방법 with ViewModel, LiveData

iron9462 2022. 3. 23. 00:26

새 직장에와서 거래소 안드로이드 앱을 열심히 구축하고 있습니다.

현재 개발중인 거래소앱은 Single Activity 모델로 삼아, 거의 모든 화면들은 Fragment를 사용하고 있습니다. 그래서 Fragment간 데이터 전달이 요구될때가 정말 많은데, 저희는 아래 샘플 프로젝트 처럼 구성하여 사용하고 있습니다.

 

+ 실제 적용된 아키텍쳐나 다른 라이브러리들의 사용은 제외하고 샘플 프로젝트로 간단히 설명을 드리겠습니다.

 

MainActivity와 AFragment, BFragment가 화면에 동시에 보이고 있는 상황이라고 가정해봅시다. 어떤 동일한 상태에 따라 각 화면에서의 처리는 어떻게 할 수 있을까요? 단순히 Intent를 사용해서 데이터를 주고받을 수는 있을 것 같습니다. 하지만, 데이터가 많아지거나 Activity 또는 Fragment의 개수가 많아지고 그에따른 전달도 많아지면 어떻게 할 것인가요? 또한 생명주기 문제까지 추가되면 어떻게 될까요? 당연히 코드가 복잡해질뿐더러 원치 않는 보일러 플레이트성의 코드들이 많아질 것 입니다. 저희가 지향하는 좋은 아키텍쳐의 관점과도 멀어질 것 이고, 클린 코드와도 괴리감이 있는 코드가 작성이 될 것 입니다. (이것이 잘 이해되지 않으신다면, 다수의 Activity 또는 Fragment간 데이터 전달을 직접 처리해보시고 느껴보시길 권해드립니다)

 

샘플 프로젝트는 아래와 같습니다. 화면이 어떤식으로 구성되어 있는지 변화를 주었고, 어떻게 반응하는지 확인해보시면 됩니다. (MainActivity > AFragment > BFragment)

 

 

MainActivity, AFragment, BFragment내에서 각각 구성된 Button을 클릭하는데 각 화면에서 셋팅된 TextView의 데이터가 동일하게 바뀌고 있습니다.

 

만약에, MainActivity에서 전달한 데이터가 AFragment를 거친 후 BFragment로 데이터를 전달해주거나, 또 반대로 코드가 구성이 되었을 경우 몇개 안되는 화면인데도 불구하고 코드가 확연하게 복잡해질 것 입니다. 

 

우선, 제가 진행한 방식의 프로젝트를 구성하기 위한 전제 조건으로는 JetPack에 포함된 ViewModel과 LiveData 사용이 필요합니다. 이 포스팅에서는 해당 라이브러리들의 설명은 줄이겠습니다. 지금은 흔하디 흔한 API들일지 모르지만, 혹시 모르시다면 직접 문서에 찾아가 어떤 장점이있고 어떻게 사용하는지 꼭 확인해보시길 바랍니다. 

 

단순하게 코드가 복잡해진다는 단점만 말씀을 드린 것 같은데, Activity와 Fragment의 생명주기 관점에서 데이터 저장과 관련된 문제점도 있고 그로인한 메모리 누수 문제도 있을 수 있습니다. 아래의 코드 구성을 통해 이러한 문제를 해결할 수 도 있거니와 아키텍쳐적으로 얻을 수 있는 이점, 어떤 상태에 대한 유연한 처리가 가능하다는 등 여러가지 장점들이 있습니다.

 

아래 소스파일을 모두 올려드렸으니 도움이 되시길 바랍니다.

 

감사합니다.

 

 

- MainActivity

class MainActivity : AppCompatActivity() {

    private val sampleViewModel by viewModels<SampleViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        changeButton.setOnClickListener { sampleViewModel.switchState() }

        sampleViewModel.currentState.observe(this, {
            when(it) {
                InitState -> stateTextView.text = "It will be changed"
                FirstState -> stateTextView.text = "First State"
                SecondState -> stateTextView.text = "Second State"
                else -> stateTextView.text = "Third State"
            }
        })
    }
}
<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/constraintLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="190dp"
        android:gravity="center"
        android:text="MainActivity"
        app:layout_constraintEnd_toStartOf="@id/stateTextView"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/changeButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/gray_400"
        android:padding="8dp"
        android:text="Change State"
        app:layout_constraintBottom_toTopOf="@id/dividerView"
        app:layout_constraintEnd_toStartOf="@id/stateTextView"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.7" />

    <TextView
        android:id="@+id/stateTextView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toEndOf="@id/textView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toTopOf="@id/dividerView"
        android:gravity="center"
        android:text="It will be chanegd"/>

    <View
        android:id="@+id/dividerView"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="@color/black"
        app:layout_constraintTop_toBottomOf="@id/textView" />

    <androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/fragment_container_view"
        android:name="com.iron.viewmodel.practice.AFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@id/dividerView" />
    
</androidx.constraintlayout.widget.ConstraintLayout>

- AFragment

class AFragment : Fragment(R.layout.fragment_a) {

    private val sampleViewModel by activityViewModels<SampleViewModel>()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        changeButton.setOnClickListener { sampleViewModel.switchState() }

        sampleViewModel.currentState.observe(this, {
            when(it) {
                InitState -> stateTextView.text = "It will be changed"
                FirstState -> stateTextView.text = "First State"
                SecondState -> stateTextView.text = "Second State"
                else -> stateTextView.text = "Third State"
            }
        })
    }
}
<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:gravity="center"
        android:text="AFragment"
        app:layout_constraintBottom_toTopOf="@id/dividerView"
        app:layout_constraintEnd_toStartOf="@id/stateTextView"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/changeButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/gray_400"
        android:padding="8dp"
        android:text="Change State"
        app:layout_constraintBottom_toTopOf="@id/dividerView"
        app:layout_constraintEnd_toStartOf="@id/stateTextView"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.7" />

    <TextView
        android:id="@+id/stateTextView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:gravity="center"
        android:text="It will be chanegd"
        app:layout_constraintBottom_toTopOf="@id/dividerView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/textView"
        app:layout_constraintTop_toTopOf="parent" />

    <View
        android:id="@+id/dividerView"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="@color/black"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fragmentContainerView"
        android:name="com.iron.viewmodel.practice.BFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@id/dividerView" />

</androidx.constraintlayout.widget.ConstraintLayout>

- BFragment

class BFragment : Fragment(R.layout.fragment_b) {

    private val sampleViewModel by activityViewModels<SampleViewModel>()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        changeButton.setOnClickListener { sampleViewModel.switchState() }

        sampleViewModel.currentState.observe(this, {
            when(it) {
                InitState -> stateTextView.text = "It will be changed"
                FirstState -> stateTextView.text = "First State"
                SecondState -> stateTextView.text = "Second State"
                else -> stateTextView.text = "Third State"
            }
        })
    }
}
<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="BFragment"
        app:layout_constraintEnd_toStartOf="@id/stateTextView"
        app:layout_constraintStart_toStartOf="parent" />

    <TextView
        android:id="@+id/changeButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/gray_400"
        android:padding="8dp"
        android:text="Change State"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/stateTextView"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.7" />

    <TextView
        android:id="@+id/stateTextView"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="It will be chanegd"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/textView" />

</androidx.constraintlayout.widget.ConstraintLayout>