프래그먼트와 통신

프래그먼트를 재사용하려면 자체 레이아웃과 동작을 정의하는 완전히 독립된 구성요소로 각 프래그먼트를 빌드합니다. 이러한 재사용 가능한 프래그먼트를 정의한 후에는 활동 및 애플리케이션 로직과 연결하여 전반적인 복합 UI를 실현할 수 있습니다.

사용자 이벤트에 올바르게 반응하거나 상태 정보를 공유하려면 활동과 활동의 프래그먼트 간 또는 두 개 이상의 프래그먼트 간에 통신 채널이 있어야 할 때가 많습니다. 프래그먼트를 독립적으로 유지하려면 프래그먼트가 다른 프래그먼트 또는 호스트 활동과 직접 통신해서는 안 됩니다.

Fragment 라이브러리는 공유 ViewModel 및 Fragment Result API라는 두 가지 통신 옵션을 제공합니다. 권장되는 옵션은 사용 사례에 따라 다릅니다. 영구 데이터를 모든 맞춤 API와 공유하려면 ViewModel을 사용해야 합니다. Bundle에 배치할 수 있는 데이터가 포함된 일회성 결과의 경우 Fragment Result API를 사용해야 합니다.

다음 섹션에서는 ViewModel 및 Fragment Result API를 사용하여 프래그먼트와 활동 간에 통신하는 방법을 보여 줍니다.

ViewModel을 사용하여 데이터 공유

ViewModel은 여러 프래그먼트 간에 또는 프래그먼트와 호스트 활동 간에 데이터를 공유해야 할 때 적합합니다. ViewModel 객체는 UI 데이터를 저장하고 관리합니다. ViewModel에 관한 자세한 내용은 ViewModel 개요를 참고하세요.

호스트 활동과 데이터 공유

때에 따라 프래그먼트와 호스트 활동 간에 데이터를 공유해야 할 수 있습니다. 예를 들어 프래그먼트 내의 상호작용에 기반하여 전역 UI 구성요소를 전환해야 할 수 있습니다.

다음 ItemViewModel을 고려하세요.

Kotlin

class ItemViewModel : ViewModel() {
    private val mutableSelectedItem = MutableLiveData<Item>()
    val selectedItem: LiveData<Item> get() = mutableSelectedItem

    fun selectItem(item: Item) {
        mutableSelectedItem.value = item
    }
}

자바

public class ItemViewModel extends ViewModel {
    private final MutableLiveData<Item> selectedItem = new MutableLiveData<Item>();
    public void selectItem(Item item) {
        selectedItem.setValue(item);
    }
    public LiveData<Item> getSelectedItem() {
        return selectedItem;
    }
}

이 예에서 저장되는 데이터는 MutableLiveData 클래스로 래핑됩니다. LiveData는 수명 주기를 인식하는 관찰 가능한 데이터 홀더 클래스입니다. MutableLiveData의 값은 변경될 수 있습니다. LiveData에 관한 자세한 내용은 LiveData 개요를 참고하세요.

프래그먼트와 호스트 활동은 모두 활동을 ViewModelProvider 생성자에 전달하여 활동 범위가 있는 ViewModel의 공유 인스턴스를 검색할 수 있습니다. ViewModelProviderViewModel를 인스턴스화하거나 이미 존재하는 경우 ViewModel을 검색합니다. 두 구성요소 모두 이 데이터를 관찰하고 수정할 수 있습니다.

Kotlin

class MainActivity : AppCompatActivity() {
    // Using the viewModels() Kotlin property delegate from the activity-ktx
    // artifact to retrieve the ViewModel in the activity scope
    private val viewModel: ItemViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel.selectedItem.observe(this, Observer { item ->
            // Perform an action with the latest item data
        })
    }
}

class ListFragment : Fragment() {
    // Using the activityViewModels() Kotlin property delegate from the
    // fragment-ktx artifact to retrieve the ViewModel in the activity scope
    private val viewModel: ItemViewModel by activityViewModels()

    // Called when the item is clicked
    fun onItemClicked(item: Item) {
        // Set a new item
        viewModel.selectItem(item)
    }
}

자바

public class MainActivity extends AppCompatActivity {
    private ItemViewModel viewModel;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewModel = new ViewModelProvider(this).get(ItemViewModel.class);
        viewModel.getSelectedItem().observe(this, item -> {
            // Perform an action with the latest item data
        });
    }
}

public class ListFragment extends Fragment {
    private ItemViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewModel = new ViewModelProvider(requireActivity()).get(ItemViewModel.class);

        ...

        items.setOnClickListener(item -> {
            // Set a new item
            viewModel.select(item);
        });
    }
}

프래그먼트 간 데이터 공유

동일한 활동에 있는 두 개 이상의 프래그먼트는 서로 통신해야 할 때가 많습니다. 예를 들어 목록을 표시하는 프래그먼트 하나와 사용자가 목록에 다양한 필터를 적용할 수 있는 프래그먼트 하나를 생각해 보세요. 이 경우는 프래그먼트가 직접 통신하지 않고서는 구현하기가 간단하지 않을 수 있으며 이는 프래그먼트가 더 이상 독립적이지 않음을 의미합니다. 또한 두 프래그먼트는 다른 프래그먼트가 아직 만들어지지 않았거나 표시되지 않은 시나리오를 처리해야 합니다.

이러한 프래그먼트는 이 통신을 처리하기 위해 활동 범위를 사용하여 ViewModel을 공유할 수 있습니다. 이러한 방식으로 ViewModel을 공유하면 프래그먼트는 서로에 관해 알 필요가 없으며 활동은 통신을 용이하게 하기 위해 어떤 작업도 할 필요가 없습니다.

다음 예는 두 프래그먼트가 공유 ViewModel을 사용하여 통신하는 방법을 보여 줍니다.

Kotlin

class ListViewModel : ViewModel() {
    val filters = MutableLiveData<Set<Filter>>()

    private val originalList: LiveData<List<Item>>() = ...
    val filteredList: LiveData<List<Item>> = ...

    fun addFilter(filter: Filter) { ... }

    fun removeFilter(filter: Filter) { ... }
}

class ListFragment : Fragment() {
    // Using the activityViewModels() Kotlin property delegate from the
    // fragment-ktx artifact to retrieve the ViewModel in the activity scope
    private val viewModel: ListViewModel by activityViewModels()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.filteredList.observe(viewLifecycleOwner, Observer { list ->
            // Update the list UI
        }
    }
}

class FilterFragment : Fragment() {
    private val viewModel: ListViewModel by activityViewModels()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.filters.observe(viewLifecycleOwner, Observer { set ->
            // Update the selected filters UI
        }
    }

    fun onFilterSelected(filter: Filter) = viewModel.addFilter(filter)

    fun onFilterDeselected(filter: Filter) = viewModel.removeFilter(filter)
}

자바

public class ListViewModel extends ViewModel {
    private final MutableLiveData<Set<Filter>> filters = new MutableLiveData<>();

    private final LiveData<List<Item>> originalList = ...;
    private final LiveData<List<Item>> filteredList = ...;

    public LiveData<List<Item>> getFilteredList() {
        return filteredList;
    }

    public LiveData<Set<Filter>> getFilters() {
        return filters;
    }

    public void addFilter(Filter filter) { ... }

    public void removeFilter(Filter filter) { ... }
}

public class ListFragment extends Fragment {
    private ListViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewModel = new ViewModelProvider(requireActivity()).get(ListViewModel.class);
        viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> {
            // Update the list UI
        });
    }
}

public class FilterFragment extends Fragment {
    private ListViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        viewModel = new ViewModelProvider(requireActivity()).get(ListViewModel.class);
        viewModel.getFilters().observe(getViewLifecycleOwner(), set -> {
            // Update the selected filters UI
        });
    }

    public void onFilterSelected(Filter filter) {
        viewModel.addFilter(filter);
    }

    public void onFilterDeselected(Filter filter) {
        viewModel.removeFilter(filter);
    }
}

두 프래그먼트는 모두 호스트 활동을 ViewModelProvider의 범위로 사용합니다. 프래그먼트가 동일한 범위를 사용하므로 동일한 ViewModel 인스턴스를 수신하여 서로 통신할 수 있습니다.

상위 프래그먼트와 하위 프래그먼트 간 데이터 공유

하위 프래그먼트로 작업할 때 상위 프래그먼트와 그 하위 프래그먼트는 서로 데이터를 공유해야 할 수도 있습니다. 이러한 프래그먼트 간에 데이터를 공유하려면 상위 프래그먼트를 ViewModel 범위로 사용하세요.

Kotlin

class ListFragment: Fragment() {
    // Using the viewModels() Kotlin property delegate from the fragment-ktx
    // artifact to retrieve the ViewModel
    private val viewModel: ListViewModel by viewModels()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.filteredList.observe(viewLifecycleOwner, Observer { list ->
            // Update the list UI
        }
    }
}

class ChildFragment: Fragment() {
    // Using the viewModels() Kotlin property delegate from the fragment-ktx
    // artifact to retrieve the ViewModel using the parent fragment's scope
    private val viewModel: ListViewModel by viewModels({requireParentFragment()})
    ...
}

자바

public class ListFragment extends Fragment {
    private ListViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        viewModel = new ViewModelProvider(this).get(ListViewModel.class);
        viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> {
            // Update the list UI
        }
    }
}

public class ChildFragment extends Fragment {
    private ListViewModel viewModel;
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        viewModel = new ViewModelProvider(requireParentFragment()).get(ListViewModel.class);
        ...
    }
}

Navigation Graph로 ViewModel의 범위 지정

Navigation 라이브러리를 사용하고 있다면 ViewModel의 범위를 대상 NavBackStackEntry의 수명 주기로 지정할 수도 있습니다. 예를 들어 ViewModel의 범위를 ListFragmentNavBackStackEntry로 지정할 수 있습니다.

Kotlin

class ListFragment: Fragment() {
    // Using the navGraphViewModels() Kotlin property delegate from the fragment-ktx
    // artifact to retrieve the ViewModel using the NavBackStackEntry scope
    // R.id.list_fragment == the destination id of the ListFragment destination
    private val viewModel: ListViewModel by navGraphViewModels(R.id.list_fragment)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.filteredList.observe(viewLifecycleOwner, Observer { item ->
            // Update the list UI
        }
    }
}

자바

public class ListFragment extends Fragment {
    private ListViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
    NavController navController = NavHostFragment.findNavController(this);
        NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.list_fragment)

        viewModel = new ViewModelProvider(backStackEntry).get(ListViewModel.class);
        viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> {
            // Update the list UI
        }
    }
}

ViewModel의 범위를 NavBackStackEntry로 지정하는 방법에 관한 자세한 내용은 Navigation 구성요소와 프로그래매틱 방식으로 상호작용을 참고하세요.

Fragment Result API를 사용하여 결과 가져오기

경우에 따라 두 프래그먼트 간에 또는 프래그먼트와 호스트 활동 간에 일회성 값을 전달해야 할 수 있습니다. 예를 들어 QR 코드를 읽고 이전 프래그먼트로 데이터를 다시 전달하는 프래그먼트가 있을 수 있습니다. Fragment 1.3.0-alpha04부터 각 FragmentManagerFragmentResultOwner를 구현합니다. 즉, FragmentManager는 프래그먼트 결과의 중앙 저장소 역할을 할 수 있습니다. 이번 변경으로 구성요소가 서로를 직접 참조하지 않아도 프래그먼트 결과를 설정하고 이러한 결과를 수신 대기하여 구성요소가 서로 통신할 수 있습니다.

프래그먼트 간 결과 전달

프래그먼트 B에서 프래그먼트 A로 데이터를 다시 전달하려면 우선, 결과를 수신하는 프래그먼트인 프래그먼트 A에서 결과 리스너를 설정합니다. 다음 예와 같이 프래그먼트 A의 FragmentManager에서 setFragmentResultListener()를 호출합니다.

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // Use the Kotlin extension in the fragment-ktx artifact
    setFragmentResultListener("requestKey") { requestKey, bundle ->
        // We use a String here, but any type that can be put in a Bundle is supported
        val result = bundle.getString("bundleKey")
        // Do something with the result
    }
}

자바

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getParentFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() {
        @Override
        public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
            // We use a String here, but any type that can be put in a Bundle is supported
            String result = bundle.getString("bundleKey");
            // Do something with the result
        }
    });
}
프래그먼트 B는 프래그먼트 관리자를 사용하여 프래그먼트 A에 데이터를 전송합니다.
그림 1. FragmentManager를 사용하여 프래그먼트 A로 데이터를 전송하는 프래그먼트 B

결과를 생성하는 프래그먼트인 프래그먼트 B에서 동일한 requestKey를 사용하여 동일한 FragmentManager에 결과를 설정해야 합니다. 이 작업은 setFragmentResult() API를 사용하여 실행할 수 있습니다.

Kotlin

button.setOnClickListener {
    val result = "result"
    // Use the Kotlin extension in the fragment-ktx artifact
    setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}

자바

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Bundle result = new Bundle();
        result.putString("bundleKey", "result");
        getParentFragmentManager().setFragmentResult("requestKey", result);
    }
});

그러면 프래그먼트 A가 결과를 수신하고 STARTED 상태가 되면 리스너 콜백을 실행합니다.

주어진 키에는 단일 리스너와 결과만 있을 수 있습니다. 동일한 키에 setFragmentResult()를 두 번 이상 호출하는 경우 그리고 리스너가 STARTED 상태가 아닌 경우 시스템은 대기 중인 결과를 업데이트된 결과로 바꿉니다. 결과를 수신할 관련 리스너 없이 결과를 설정하면 결과는 동일한 키로 리스너를 설정할 때까지 FragmentManager에 저장됩니다. 리스너가 결과를 수신하고 onFragmentResult() 콜백을 실행하면 결과는 삭제됩니다. 이 동작에는 다음 두 가지 의미가 있습니다.

  • 백 스택의 프래그먼트는 표시되어 STARTED 상태가 될 때까지 결과를 수신하지 않습니다.
  • 결과를 수신 대기하는 프래그먼트가 STARTED 상태인 경우 결과가 설정되면 리스너의 콜백이 즉시 실행됩니다.

프래그먼트 결과 테스트

FragmentScenario를 사용하여 setFragmentResult()setFragmentResultListener() 호출을 테스트합니다. launchFragmentInContainer 또는 launchFragment를 사용하여 테스트 중인 프래그먼트의 시나리오를 만들고 테스트 중이 아닌 메서드를 수동으로 호출합니다.

setFragmentResultListener()를 테스트하려면 setFragmentResultListener()를 호출하는 프래그먼트로 시나리오를 만듭니다. 그런 다음 setFragmentResult()를 직접 호출하여 다음과 같이 결과를 확인합니다.

@Test
fun testFragmentResultListener() {
    val scenario = launchFragmentInContainer<ResultListenerFragment>()
    scenario.onFragment { fragment ->
        val expectedResult = "result"
        fragment.parentFragmentManager.setFragmentResult("requestKey", bundleOf("bundleKey" to expectedResult))
        assertThat(fragment.result).isEqualTo(expectedResult)
    }
}

class ResultListenerFragment : Fragment() {
    var result : String? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Use the Kotlin extension in the fragment-ktx artifact
        setFragmentResultListener("requestKey") { requestKey, bundle ->
            result = bundle.getString("bundleKey")
        }
    }
}

setFragmentResult()를 테스트하려면 setFragmentResult()를 호출하는 프래그먼트로 시나리오를 만듭니다. 그런 다음 setFragmentResultListener()를 직접 호출하여 다음과 같이 결과를 확인합니다.

@Test
fun testFragmentResult() {
    val scenario = launchFragmentInContainer<ResultFragment>()
    lateinit var actualResult: String?
    scenario.onFragment { fragment ->
        fragment.parentFragmentManager
                .setFragmentResultListener("requestKey") { requestKey, bundle ->
            actualResult = bundle.getString("bundleKey")
        }
    }
    onView(withId(R.id.result_button)).perform(click())
    assertThat(actualResult).isEqualTo("result")
}

class ResultFragment : Fragment(R.layout.fragment_result) {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        view.findViewById(R.id.result_button).setOnClickListener {
            val result = "result"
            // Use the Kotlin extension in the fragment-ktx artifact
            setFragmentResult("requestKey", bundleOf("bundleKey" to result))
        }
    }
}

상위 및 하위 프래그먼트 간 결과 전달

하위 프래그먼트의 결과를 상위 프래그먼트로 전달하려면 상위 프래그먼트는 setFragmentResultListener()를 호출할 때 getParentFragmentManager() 대신 getChildFragmentManager()를 사용해야 합니다.

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // We set the listener on the child fragmentManager
    childFragmentManager.setFragmentResultListener("requestKey") { key, bundle ->
        val result = bundle.getString("bundleKey")
        // Do something with the result
    }
}

자바

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // We set the listener on the child fragmentManager
    getChildFragmentManager()
        .setFragmentResultListener("requestKey", this, new FragmentResultListener() {
            @Override
            public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
                String result = bundle.getString("bundleKey");
                // Do something with the result
            }
        });
}
하위 프래그먼트는 프래그먼트 관리자를 사용하여 상위 프래그먼트에 결과를 전송할 수 있습니다.
그림 2. FragmentManager를 사용하여 상위 프래그먼트에 결과를 전송할 수 있는 하위 프래그먼트

하위 프래그먼트는 FragmentManager에 결과를 설정합니다. 그러면 다음과 같이 프래그먼트가 STARTED 상태가 되면 상위 프래그먼트에서 결과를 수신합니다.

Kotlin

button.setOnClickListener {
    val result = "result"
    // Use the Kotlin extension in the fragment-ktx artifact
    setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}

자바

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Bundle result = new Bundle();
        result.putString("bundleKey", "result");
        // The child fragment needs to still set the result on its parent fragment manager
        getParentFragmentManager().setFragmentResult("requestKey", result);
    }
});

호스트 활동에서 결과 수신

호스트 활동에서 프래그먼트 결과를 수신하려면 getSupportFragmentManager()를 사용하여 프래그먼트 관리자에서 결과 리스너를 설정합니다.

Kotlin

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportFragmentManager
                .setFragmentResultListener("requestKey", this) { requestKey, bundle ->
            // We use a String here, but any type that can be put in a Bundle is supported
            val result = bundle.getString("bundleKey")
            // Do something with the result
        }
    }
}

자바

class MainActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getSupportFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() {
            @Override
            public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) {
                // We use a String here, but any type that can be put in a Bundle is supported
                String result = bundle.getString("bundleKey");
                // Do something with the result
            }
        });
    }
}