탐색 구성요소로 이전

탐색 구성요소는 앱에서 복잡한 탐색, 전환 애니메이션, 딥 링크, 화면 간 전달되는 컴파일 시간 확인 인수를 관리할 수 있는 라이브러리입니다.

이 문서는 탐색 구성요소를 사용할 수 있도록 기존 앱을 이전하기 위한 일반적인 용도의 가이드로 활용할 수 있습니다.

상위 수준에서는 이전에 다음 단계가 포함됩니다.

  1. 화면별 UI 로직을 활동 외부로 이동 - 앱의 UI 로직을 활동 외부로 이동하여 각 화면의 구현을 프래그먼트나 맞춤 대상으로 위임하면서 동시에 각 활동이 Toolbar 같은 전역 탐색 UI 구성요소의 로직만 소유하도록 합니다.

  2. 탐색 구성요소 통합 - 각 활동에서 활동이 관리하는 프래그먼트가 하나 이상 포함된 탐색 그래프를 빌드하고 프래그먼트 트랜잭션을 탐색 구성요소 작업으로 대체합니다.

  3. 활동 대상 추가 - 활동 대상을 사용해 startActivity() 호출을 작업으로 대체합니다.

  4. 활동 결합 - 여러 활동이 공통 레이아웃을 공유하는 경우 탐색 그래프를 결합합니다.

사전 준비 사항

이 가이드에서는 앱이 이미 이전되어 AndroidX 라이브러리를 사용할 수 있다고 가정합니다. 아직 앱을 이전하지 않았다면 계속 진행하기 전에 AndroidX를 사용할 수 있도록 프로젝트를 이전하세요.

화면별 UI 로직을 활동 외부로 이동

활동은 앱과 Android 간의 그래픽 상호작용을 용이하게 하는 시스템 수준의 구성요소로 어떤 활동을 실행할 수 있는지 Android에서 알 수 있도록 활동이 앱의 매니페스트에 등록됩니다. 활동 클래스를 사용하면 앱의 UI가 포그라운드로 전환되거나 포그라운드를 종료하거나 회전하는 등의 상태일 때 앱에서 Android 변경사항에 반응할 수 있습니다. 또한 활동은 화면 간에 상태를 공유하기 위한 장소 역할을 할 수 있습니다.

앱의 컨텍스트 내에서 활동은 탐색의 호스트 역할을 하며 화면 간 전환, 데이터 전달 등의 방법에 관한 논리와 지식을 보유해야 합니다. 하지만 더 작고 재사용 가능한 UI 부분으로 UI 세부정보를 관리하는 것이 좋습니다. 이 패턴에 권장되는 구현은 프래그먼트입니다. 프래그먼트 사용의 장점을 자세히 알아보려면 단일 활동: 왜, 언제, 어떻게를 참고하세요. 탐색은 탐색-프래그먼트 종속 항목을 통해 프래그먼트를 지원하며 맞춤 대상 유형도 지원합니다.

앱에서 프래그먼트를 사용하지 않는 경우 먼저 프래그먼트를 사용하도록 앱의 각 화면을 이전해야 합니다. 이 시점에서는 활동을 삭제하지 않으며 오히려 화면을 나타내는 프래그먼트를 만들고 책임에 따라 UI 로직을 분리해야 합니다.

프래그먼트 도입

프래그먼트 도입 과정을 설명하기 위해 두 화면(제품 목록 화면과 제품 세부정보 화면)으로 구성된 애플리케이션을 예로 시작해 보겠습니다. 사용자가 목록 화면에서 제품을 클릭하면 제품에 관해 자세히 알아볼 수 있도록 세부정보 화면으로 이동합니다.

이 예에서 목록 및 세부정보 화면은 현재 별도의 활동입니다.

UI를 호스팅할 새 레이아웃 만들기

프래그먼트를 도입하려면 먼저 프래그먼트를 호스팅할 활동의 새 레이아웃 파일을 만듭니다. 이 파일은 활동의 현재 콘텐츠 뷰 레이아웃을 대체합니다.

다음 product_list_host 예에 표시된 대로 FrameLayout을 사용하여 간단히 볼 수 있습니다.

<FrameLayout
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:id="@+id/main_content"
   android:layout_height="match_parent"
   android:layout_width="match_parent" />

id 속성은 나중에 프래그먼트를 추가할 콘텐츠 섹션을 가리킵니다.

그다음에 활동의 onCreate() 함수에서 활동의 onCreate 함수에 있는 레이아웃 파일 참조를 수정하여 이 새로운 레이아웃 파일을 가리킵니다.

Kotlin

class ProductListActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // Replace setContentView(R.layout.product_list) with the line below
        setContentView(R.layout.product_list_host)
        ...
    }
}

자바

public class ProductListActivity extends AppCompatActivity {
    ...
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        ...
        // Replace setContentView(R.layout.product_list); with the line below
        setContentView(R.layout.product_list_host);
        ...
    }
}

기존 레이아웃(이 예에서는 product_list)은 개발자가 만들려는 프래그먼트의 루트 뷰로 사용됩니다.

프래그먼트 만들기

화면의 UI를 관리할 새 프래그먼트를 만듭니다. 새 프래그먼트 이름은 활동 호스트 이름과 일치하는 것이 좋습니다. 예를 들어 아래 스니펫에서는 ProductListFragment를 사용합니다.

Kotlin

class ProductListFragment : Fragment() {
    // Leave empty for now.
}

Java

public class ProductListFragment extends Fragment {
    // Leave empty for now.
}

활동 로직을 프래그먼트로 이동

프래그먼트 정의가 적용된 상태에서 다음 단계는 이 화면의 UI 로직을 활동에서 이 새로운 프래그먼트로 이동하는 것입니다. 활동 기반 아키텍처 관점에서 활동의 onCreate() 함수에서 발생하는 많은 뷰 생성 로직이 있을 수 있습니다.

다음은 이동해야 하는 UI 로직이 포함된 활동 기반 화면의 예입니다.

Kotlin

class ProductListActivity : AppCompatActivity() {

    // Views and/or ViewDataBinding references, Adapters...
    private lateinit var productAdapter: ProductAdapter
    private lateinit var binding: ProductListActivityBinding

    ...

    // ViewModels, System Services, other Dependencies...
    private val viewModel: ProductListViewModel by viewModels()

    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // View initialization logic
        DataBindingUtil.setContentView(this, R.layout.product_list_activity)

        // Post view initialization logic
        // Connect adapters
        productAdapter = ProductAdapter(productClickCallback)
        binding.productsList.setAdapter(productAdapter)

        // Initialize view properties, set click listeners, etc.
        binding.productsSearchBtn.setOnClickListener {...}

        // Subscribe to state
        viewModel.products.observe(this, Observer { myProducts ->
            ...
        })

        // ...and so on
    }
   ...
}

자바

public class ProductListActivity extends AppCompatActivity {

    // Views and/or ViewDataBinding references, adapters...
    private ProductAdapter productAdapter;
    private ProductListActivityBinding binding;

    ...

    // ViewModels, system services, other dependencies...
    private ProductListViewModel viewModel;

    ...

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // View initialization logic
        DataBindingUtil.setContentView(this, R.layout.product_list_activity);

        // Post view initialization logic
        // Connect adapters
        productAdapter = new ProductAdapter(productClickCallback);
        binding.productsList.setAdapter(productAdapter);

        // Initialize ViewModels and other dependencies
        ProductListViewModel viewModel = new ViewModelProvider(this).get(ProductListViewModel.java);

        // Initialize view properties, set click listeners, etc.
        binding.productsSearchBtn.setOnClickListener(v -> { ... });

        // Subscribe to state
        viewModel.getProducts().observe(this, myProducts ->
            ...
       );

       // ...and so on
   }

다음 예에서와 같이 활동에서 사용자가 다음 화면으로 이동하는 시기와 방법을 제어하고 있을 수 있습니다.

Kotlin

    // Provided to ProductAdapter in ProductListActivity snippet.
    private val productClickCallback = ProductClickCallback { product ->
        show(product)
    }

    fun show(product: Product) {
        val intent = Intent(this, ProductActivity::class.java)
        intent.putExtra(ProductActivity.KEY_PRODUCT_ID, product.id)
        startActivity(intent)
    }

자바

// Provided to ProductAdapter in ProductListActivity snippet.
private ProductClickCallback productClickCallback = this::show;

private void show(Product product) {
    Intent intent = new Intent(this, ProductActivity.class);
    intent.putExtra(ProductActivity.KEY_PRODUCT_ID, product.getId());
    startActivity(intent);
}

프래그먼트 내부에서 활동에 남아있는 탐색 로직만 사용하여 onCreateView()onViewCreated() 사이에 이 작업을 배포합니다.

Kotlin

class ProductListFragment : Fragment() {

    private lateinit var binding: ProductListFragmentBinding
    private val viewModel: ProductListViewModel by viewModels()

     // View initialization logic
    override fun onCreateView(inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?): View? {
        binding = DataBindingUtil.inflate(
                inflater,
                R.layout.product_list,
                container,
                false
        )
        return binding.root
    }

    // Post view initialization logic
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // Connect adapters
        productAdapter = ProductAdapter(productClickCallback)
        binding.productsList.setAdapter(productAdapter)

        // Initialize view properties, set click listeners, etc.
        binding.productsSearchBtn.setOnClickListener {...}

        // Subscribe to state
        viewModel.products.observe(this, Observer { myProducts ->
            ...
        })

        // ...and so on
    }

    // Provided to ProductAdapter
    private val productClickCallback = ProductClickCallback { product ->
        if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
            (requireActivity() as ProductListActivity).show(product)
        }
    }
    ...
}

자바

public class ProductListFragment extends Fragment {

    private ProductAdapter productAdapter;
    private ProductListFragmentBinding binding;

    // View initialization logic
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
            @Nullable ViewGroup container,
            @Nullable Bundle savedInstanceState) {
        binding = DataBindingUtil.inflate(
                inflater,
                R.layout.product_list_fragment,
                container,
                false);
        return binding.getRoot();
    }

    // Post view initialization logic
    @Override
    public void onViewCreated(@NonNull View view,
            @Nullable Bundle savedInstanceState) {

        // Connect adapters
        binding.productsList.setAdapter(productAdapter);

        // Initialize ViewModels and other dependencies
        ProductListViewModel viewModel = new ViewModelProvider(this)
                .get(ProductListViewModel.class);

        // Initialize view properties, set click listeners, etc.
        binding.productsSearchBtn.setOnClickListener(...)

        // Subscribe to state
        viewModel.getProducts().observe(this, myProducts -> {
            ...
       });

       // ...and so on

    // Provided to ProductAdapter
    private ProductClickCallback productClickCallback = new ProductClickCallback() {
        @Override
        public void onClick(Product product) {
            if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
                ((ProductListActivity) requireActivity()).show(product);
            }
        }
    };
    ...
}

ProductListFragment에는 레이아웃을 확장하고 연결할 setContentView() 호출이 없습니다. 프래그먼트에서 onCreateView()가 루트 뷰를 초기화합니다. onCreateView()는 레이아웃 리소스 파일에 따라 루트 뷰를 확장하는 데 사용될 수 있는 LayoutInflater의 인스턴스를 받습니다. 이 예에서는 레이아웃 자체를 변경할 필요가 없으므로 활동에서 사용된 기존 product_list 레이아웃을 다시 사용합니다.

활동의 onStart(), onResume(), onPause() 또는 onStop() 함수에 탐색과 관련이 없는 UI 로직이 있다면 이러한 로직을 프래그먼트에서 이름이 동일한 상응하는 함수로 이동할 수 있습니다.

호스트 활동에서 프래그먼트 초기화

모든 UI 로직을 프래그먼트로 이동한 후에는 활동에 탐색 로직만 남아있어야 합니다.

Kotlin

class ProductListActivity : AppCompatActivity() {

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

    fun show(product: Product) {
        val intent = Intent(this, ProductActivity::class.java)
        intent.putExtra(ProductActivity.KEY_PRODUCT_ID, product.id)
        startActivity(intent)
    }
}

자바

public class ProductListActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.product_list_host);
    }

    public void show(Product product) {
        Intent intent = new Intent(this, ProductActivity.class);
        intent.putExtra(ProductActivity.KEY_PRODUCT_ID, product.getId());
        startActivity(intent);
    }
}

마지막 단계는 콘텐츠 뷰를 설정한 직후 onCreate()에 프래그먼트의 인스턴스를 만드는 것입니다.

Kotlin

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

    if (savedInstanceState == null) {
        val fragment = ProductListFragment()
        supportFragmentManager
                .beginTransaction()
                .add(R.id.main_content, fragment)
                .commit()
    }
}

자바

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.product_list_host);

    if (savedInstanceState == null) {
        ProductListFragment fragment = new ProductListFragment();
        getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.main_content, fragment)
                .commit();
    }
}

이 예에서와 같이 FragmentManager는 구성 변경사항에 따라 프래그먼트를 자동으로 저장하고 복원하므로 savedInstanceState가 null인 경우에만 프래그먼트를 추가하면 됩니다.

프래그먼트에 추가 인텐트 전달

활동에서 인텐트를 통해 Extras를 수신하는 경우 이러한 추가 인텐트를 프래그먼트에 인수로 직접 전달할 수 있습니다.

이 예에서 ProductDetailsFragment는 활동의 추가 인텐트에서 직접 인수를 수신합니다.

Kotlin

...

if (savedInstanceState == null) {
    val fragment = ProductDetailsFragment()

    // Intent extras and Fragment Args are both of type android.os.Bundle.
    fragment.arguments = intent.extras

    supportFragmentManager
            .beginTransaction()
            .add(R.id.main_content, fragment)
            .commit()
}

...

자바

...

if (savedInstanceState == null) {
    ProductDetailsFragment fragment = new ProductDetailsFragment();

    // Intent extras and fragment Args are both of type android.os.Bundle.
    fragment.setArguments(getIntent().getExtras());

    getSupportFragmentManager()
            .beginTransaction()
            .add(R.id.main_content, fragment)
            .commit();
}

...

이제 프래그먼트를 사용하도록 업데이트된 첫 번째 화면에서 앱 실행을 테스트할 수 있습니다. 나머지 활동 기반 화면을 계속 이전하고 이전이 끝날 때마다 테스트하세요.

탐색 구성요소 통합

프래그먼트 기반 아키텍처를 사용하게 되면 탐색 구성요소 통합을 시작할 준비가 된 것입니다.

먼저 탐색 라이브러리 출시 노트의 안내에 따라 프로젝트에 최신 탐색 종속 항목을 추가합니다.

탐색 그래프 만들기

탐색 구성요소는 앱의 뷰가 표시되는 것처럼 리소스 파일의 앱 탐색 구성을 그래프로 나타냅니다. 그래프로 표시하면 앱 탐색이 코드베이스 외부에서 체계적으로 관리되며 개발자가 앱 탐색을 시각적으로 수정할 수 있습니다.

탐색 그래프를 만들려면 다음과 같이 navigation이라는 새 리소스 폴더를 만듭니다. 그래프를 추가하려면 이 디렉터리를 마우스 오른쪽 버튼으로 클릭하고 New > Navigation resource file을 선택합니다.

탐색 구성요소는 활동을 탐색용 호스트로 사용하며 사용자가 앱을 탐색할 때 개별 프래그먼트를 이 호스트로 전환합니다. 앱의 탐색을 시각적으로 배치하려면 먼저 활동 내부에 이 그래프를 호스팅할 NavHost를 구성해야 합니다. 지금은 프래그먼트를 사용 중이므로 탐색 구성요소의 기본 NavHost 구현인 NavHostFragment를 사용할 수 있습니다.

NavHostFragment는 다음 예와 같이 호스트 활동 내부에 배치된 FragmentContainerView를 통해 구성됩니다.

<androidx.fragment.app.FragmentContainerView
   android:name="androidx.navigation.fragment.NavHostFragment"
   app:navGraph="@navigation/product_list_graph"
   app:defaultNavHost="true"
   android:id="@+id/main_content"
   android:layout_width="match_parent"
   android:layout_height="match_parent" />

app:NavGraph 속성은 이 탐색 호스트와 연결된 탐색 그래프를 가리킵니다. 이 속성을 설정하면 탐색 그래프가 확장되고 NavHostFragment에 그래프 속성을 설정합니다. app:defaultNavHost 속성을 사용하면 NavHostFragment에서 시스템 뒤로 버튼을 차단합니다.

DrawerLayout 또는 BottomNavigationView와 같은 최상위 탐색을 사용 중인 경우 이 FragmentContainerView가 기본 콘텐츠 뷰 요소를 대체합니다. 예를 보려면 NavigationUI로 UI 구성요소 업데이트를 참고하세요.

레이아웃이 간단한 경우 이 FragmentContainerView 요소를 루트 ViewGroup의 하위 요소로 포함할 수 있습니다.

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

<androidx.fragment.app.FragmentContainerView
   android:id="@+id/main_content"
   android:name="androidx.navigation.fragment.NavHostFragment"
   app:navGraph="@navigation/product_list_graph"
   app:defaultNavHost="true"
   android:layout_width="match_parent"
   android:layout_height="match_parent" />

</FrameLayout>

하단에서 디자인 탭을 클릭하면 아래와 유사한 그래프가 표시됩니다. 그래프 왼쪽 상단의 대상 아래 layout_name (resource_id) 형태의 NavHost 활동 참조가 표시됩니다.

이 그래프에 프래그먼트를 추가하려면 상단의 더하기 버튼 을 클릭합니다.

탐색 구성요소에서는 개별 화면을 대상이라고 합니다. 대상은 프래그먼트, 활동 또는 맞춤 대상일 수 있습니다. 모든 유형의 대상을 그래프에 추가할 수 있지만, 활동 대상으로 이동하면 별도의 탐색 호스트 및 그래프 내에서 작업하므로 활동 대상은 터미널 대상으로 간주됩니다.

탐색 구성요소에서는 사용자가 한 대상에서 다른 대상으로 이동하는 방법을 작업이라고 합니다. 작업이 전환 애니메이션과 팝업 동작을 설명할 수도 있습니다.

프래그먼트 트랜잭션 삭제

이제 탐색 구성요소를 사용 중이므로 동일한 활동에 속한 프래그먼트 기반 화면 간을 이동하는 경우 FragmentManager 상호작용을 삭제할 수 있습니다.

앱에서 동일한 활동 또는 창 레이아웃이나 하단 탐색과 같은 최상위 탐색에 속한 여러 프래그먼트를 사용하는 경우 FragmentManagerFragmentTransactions를 사용하여 UI의 기본 콘텐츠 섹션에서 프래그먼트를 추가하거나 대체하고 있을 수 있습니다. 이제 탐색 구성요소를 사용하여 그래프 내에서 대상을 연결할 작업을 제공한 다음 NavController를 사용해 탐색함으로써 기존 방법을 대체하고 단순화할 수 있습니다.

다음은 발생할 수 있는 몇 가지 시나리오와 각 시나리오에서 사용할 수 있는 이전 방법입니다.

여러 프래그먼트를 관리하는 단일 활동

여러 프래그먼트를 관리하는 단일 활동이 있는 경우 활동 코드는 다음과 같을 수 있습니다.

Kotlin

class MainActivity : AppCompatActivity() {

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

        // Logic to load the starting destination
        //  when the Activity is first created
        if (savedInstanceState == null) {
            val fragment = ProductListFragment()
            supportFragmentManager.beginTransaction()
                    .add(R.id.fragment_container, fragment, ProductListFragment.TAG)
                    .commit()
        }
    }

    // Logic to navigate the user to another destination.
    // This may include logic to initialize and set arguments on the destination
    // fragment or even transition animations between the fragments (not shown here).
    fun navigateToProductDetail(productId: String) {
        val fragment = new ProductDetailsFragment()
        val args = Bundle().apply {
            putInt(KEY_PRODUCT_ID, productId)
        }
        fragment.arguments = args

        supportFragmentManager.beginTransaction()
                .addToBackStack(ProductDetailsFragment.TAG)
                .replace(R.id.fragment_container, fragment, ProductDetailsFragment.TAG)
                .commit()
    }
}

자바

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Logic to load the starting destination when the activity is first created.
        if (savedInstanceState == null) {
            val fragment = ProductListFragment()
            supportFragmentManager.beginTransaction()
                    .add(R.id.fragment_container, fragment, ProductListFragment.TAG)
                    .commit();
        }
    }

    // Logic to navigate the user to another destination.
    // This may include logic to initialize and set arguments on the destination
    // fragment or even transition animations between the fragments (not shown here).
    public void navigateToProductDetail(String productId) {
        Fragment fragment = new ProductDetailsFragment();
        Bundle args = new Bundle();
        args.putInt(KEY_PRODUCT_ID, productId);
        fragment.setArguments(args);

        getSupportFragmentManager().beginTransaction()
                .addToBackStack(ProductDetailsFragment.TAG)
                .replace(R.id.fragment_container, fragment, ProductDetailsFragment.TAG)
                .commit();
    }
}

소스 대상 내부에서 아래에 표시된 것처럼 일부 이벤트에 응답하여 탐색 함수를 호출할 수 있습니다.

Kotlin

class ProductListFragment : Fragment() {
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // In this example a callback is passed to respond to an item clicked
        //  in a RecyclerView
        productAdapter = ProductAdapter(productClickCallback)
        binding.productsList.setAdapter(productAdapter)
    }
    ...

    // The callback makes the call to the activity to make the transition.
    private val productClickCallback = ProductClickCallback { product ->
            (requireActivity() as MainActivity).navigateToProductDetail(product.id)
    }
}

자바

public class ProductListFragment extends Fragment  {
    ...
    @Override
    public void onViewCreated(@NonNull View view,
            @Nullable Bundle savedInstanceState) {
    // In this example a callback is passed to respond to an item clicked in a RecyclerView
        productAdapter = new ProductAdapter(productClickCallback);
        binding.productsList.setAdapter(productAdapter);
    }
    ...

    // The callback makes the call to the activity to make the transition.
    private ProductClickCallback productClickCallback = product -> (
        ((MainActivity) requireActivity()).navigateToProductDetail(product.getId())
    );
}

이 방법은 시작 대상과 작업을 설정하여 대상을 연결하고 필요에 따라 인수를 정의할 수 있도록 탐색 그래프를 업데이트하여 대체할 수 있습니다.

<navigation 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/product_list_graph"
    app:startDestination="@id/product_list">

    <fragment
        android:id="@+id/product_list"
        android:name="com.example.android.persistence.ui.ProductListFragment"
        android:label="Product List"
        tools:layout="@layout/product_list">
        <action
            android:id="@+id/navigate_to_product_detail"
            app:destination="@id/product_detail" />
    </fragment>
    <fragment
        android:id="@+id/product_detail"
        android:name="com.example.android.persistence.ui.ProductDetailFragment"
        android:label="Product Detail"
        tools:layout="@layout/product_detail">
        <argument
            android:name="product_id"
            app:argType="integer" />
    </fragment>
</navigation>

그러면 활동을 다음과 같이 업데이트할 수 있습니다.

Kotlin

class MainActivity : AppCompatActivity() {

    // No need to load the start destination, handled automatically by the Navigation component
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

자바

public class MainActivity extends AppCompatActivity {

    // No need to load the start destination, handled automatically by the Navigation component
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

활동에 더 이상 navigateToProductDetail() 메서드가 필요하지 않습니다. 다음 섹션에서는 NavController를 사용해 다음 제품 세부정보 화면으로 이동하도록 ProductListFragment를 업데이트합니다.

안전하게 인수 전달

탐색 구성요소에는 대상 및 작업에 지정된 인수에 유형 안전 액세스를 하기 위해 간단한 객체 및 빌더 클래스를 생성하는 Safe Args라는 Gradle 플러그인이 있습니다.

플러그인이 적용되면 탐색 그래프의 대상에 정의된 모든 인수가 탐색 구성요소 프레임워크에서 타겟 대상에 유형 안전 인수를 제공하는 Arguments 클래스를 생성하게 합니다. 작업을 정의하면 플러그인에서 사용자를 타겟 대상으로 이동하는 방법을 NavController에 알리는 데 사용할 수 있는 Directions 구성 클래스를 생성합니다. 작업이 인수가 필요한 대상을 가리키면 생성된 Directions 클래스에 이러한 매개변수가 필요한 생성자 메서드가 포함됩니다.

타겟 대상에 유형 안전 인수를 제공하려면 다음 예에서와 같이 프래그먼트 내에서 NavController 및 생성된 Directions 클래스를 사용합니다.

Kotlin

class ProductListFragment : Fragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // In this example a callback is passed to respond to an item clicked in a RecyclerView
        productAdapter = ProductAdapter(productClickCallback)
        binding.productsList.setAdapter(productAdapter)
    }
    ...

    // The callback makes the call to the NavController to make the transition.
    private val productClickCallback = ProductClickCallback { product ->
        val directions = ProductListDirections.navigateToProductDetail(product.id)
        findNavController().navigate(directions)
    }
}

Java

public class ProductListFragment extends Fragment  {
    ...
    @Override
    public void onViewCreated(@NonNull View view,
            @Nullable Bundle savedInstanceState) {
        // In this example a callback is passed to respond to an item clicked in a RecyclerView
        productAdapter = new ProductAdapter(productClickCallback);
        binding.productsList.setAdapter(productAdapter);
    }
    ...

    // The callback makes the call to the activity to make the transition.
    private ProductClickCallback productClickCallback = product -> {
        ProductListDirections.ViewProductDetails directions =
                ProductListDirections.navigateToProductDetail(product.getId());
        NavHostFragment.findNavController(this).navigate(directions);
    };
}

최상위 탐색

앱에서 DrawerLayout을 사용하는 경우 활동에 창 열기와 닫기 및 다른 대상으로의 이동을 관리하는 많은 구성 로직이 있을 수 있습니다.

결과 활동은 다음과 같을 수 있습니다.

Kotlin

class MainActivity : AppCompatActivity(),
    NavigationView.OnNavigationItemSelectedListener {

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

        val toolbar: Toolbar = findViewById(R.id.toolbar)
        setSupportActionBar(toolbar)

        val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
        val navView: NavigationView = findViewById(R.id.nav_view)
        val toggle = ActionBarDrawerToggle(
                this,
                drawerLayout,
                toolbar,
                R.string.navigation_drawer_open, 
                R.string.navigation_drawer_close
        )
        drawerLayout.addDrawerListener(toggle)
        toggle.syncState()

        navView.setNavigationItemSelectedListener(this)
    }

    override fun onBackPressed() {
        val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
        if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
            drawerLayout.closeDrawer(GravityCompat.START)
        } else {
            super.onBackPressed()
        }
    }

    override fun onNavigationItemSelected(item: MenuItem): Boolean {
        // Handle navigation view item clicks here.
        when (item.itemId) {
            R.id.home -> {
                val homeFragment = HomeFragment()
                show(homeFragment)
            }
            R.id.gallery -> {
                val galleryFragment = GalleryFragment()
                show(galleryFragment)
            }
            R.id.slide_show -> {
                val slideShowFragment = SlideShowFragment()
                show(slideShowFragment)
            }
            R.id.tools -> {
                val toolsFragment = ToolsFragment()
                show(toolsFragment)
            }
        }
        val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
        drawerLayout.closeDrawer(GravityCompat.START)
        return true
    }
}

private fun show(fragment: Fragment) {

    val drawerLayout = drawer_layout as DrawerLayout
    val fragmentManager = supportFragmentManager

    fragmentManager
            .beginTransaction()
            .replace(R.id.main_content, fragment)
            .commit()

    drawerLayout.closeDrawer(GravityCompat.START)
}

자바

public class MainActivity extends AppCompatActivity
        implements NavigationView.OnNavigationItemSelectedListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        DrawerLayout drawer = findViewById(R.id.drawer_layout);
        NavigationView navigationView = findViewById(R.id.nav_view);
        ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
                this,
                drawer,
                toolbar,
                R.string.navigation_drawer_open,
                R.string.navigation_drawer_close);
        drawer.addDrawerListener(toggle);
        toggle.syncState();

        navigationView.setNavigationItemSelectedListener(this);
    }

    @Override
    public void onBackPressed() {
        DrawerLayout drawer = findViewById(R.id.drawer_layout);
        if (drawer.isDrawerOpen(GravityCompat.START)) {
            drawer.closeDrawer(GravityCompat.START);
        } else {
            super.onBackPressed();
        }
    }

    @Override
    public boolean onNavigationItemSelected(MenuItem item) {
        // Handle navigation view item clicks here.
        int id = item.getItemId();

        if (id == R.id.home) {
            Fragment homeFragment = new HomeFragment();
            show(homeFragment);
        } else if (id == R.id.gallery) {
            Fragment galleryFragment = new GalleryFragment();
            show(galleryFragment);
        } else if (id == R.id.slide_show) {
            Fragment slideShowFragment = new SlideShowFragment();
            show(slideShowFragment);
        } else if (id == R.id.tools) {
            Fragment toolsFragment = new ToolsFragment();
            show(toolsFragment);
        }

        DrawerLayout drawer = findViewById(R.id.drawer_layout);
        drawer.closeDrawer(GravityCompat.START);
        return true;
    }

    private void show(Fragment fragment) {

        DrawerLayout drawerLayout = findViewById(R.id.drawer_layout);
        FragmentManager fragmentManager = getSupportFragmentManager();

        fragmentManager
                .beginTransaction()
                .replace(R.id.main_content, fragment)
                .commit();

        drawerLayout.closeDrawer(GravityCompat.START);
    }
}

프로젝트에 탐색 구성요소를 추가하고 탐색 그래프를 만들고 나면 그래프의 각 콘텐츠 대상(예: 위 예의 , 갤러리, 슬라이드쇼, 도구)을 추가합니다. 아래 표시된 것처럼 메뉴 항목 id 값이 연결된 대상 id 값과 일치하는지 확인하세요.

<!-- activity_main_drawer.xml -->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:showIn="navigation_view">

    <group android:checkableBehavior="single">
        <item
            android:id="@+id/home"
            android:icon="@drawable/ic_menu_camera"
            android:title="@string/menu_home" />
        <item
            android:id="@+id/gallery"
            android:icon="@drawable/ic_menu_gallery"
            android:title="@string/menu_gallery" />
        <item
            android:id="@+id/slide_show"
            android:icon="@drawable/ic_menu_slideshow"
            android:title="@string/menu_slideshow" />
        <item
            android:id="@+id/tools"
            android:icon="@drawable/ic_menu_manage"
            android:title="@string/menu_tools" />
    </group>
</menu>
<!-- activity_main_graph.xml -->
<navigation 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/main_graph"
    app:startDestination="@id/home">

    <fragment
        android:id="@+id/home"
        android:name="com.example.HomeFragment"
        android:label="Home"
        tools:layout="@layout/home" />

    <fragment
        android:id="@+id/gallery"
        android:name="com.example.GalleryFragment"
        android:label="Gallery"
        tools:layout="@layout/gallery" />

    <fragment
        android:id="@+id/slide_show"
        android:name="com.example.SlideShowFragment"
        android:label="Slide Show"
        tools:layout="@layout/slide_show" />

    <fragment
        android:id="@+id/tools"
        android:name="com.example.ToolsFragment"
        android:label="Tools"
        tools:layout="@layout/tools" />

</navigation>

메뉴와 그래프의 id 값을 일치시키면 메뉴 항목에 따라 자동으로 탐색을 처리하도록 이 활동의 NavController를 연결할 수 있습니다. NavControllerDrawerLayout 열기 및 닫기와 위로 및 뒤로 버튼 동작 처리를 적절하게 처리합니다.

그러면 MainActivity가 업데이트되어 NavControllerToolbarNavigationView에 연결할 수 있습니다.

예를 보려면 다음 스니펫을 참고하세요.

Kotlin

class MainActivity : AppCompatActivity()  {

    val drawerLayout by lazy { findViewById<DrawerLayout>(R.id.drawer_layout) }
    val navController by lazy {
      (supportFragmentManager.findFragmentById(R.id.main_content) as NavHostFragment).navController
    }
    val navigationView by lazy { findViewById<NavigationView>(R.id.nav_view) }

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

        val toolbar = findViewById<Toolbar>(R.id.toolbar)
        setSupportActionBar(toolbar)

        // Show and Manage the Drawer and Back Icon
        setupActionBarWithNavController(navController, drawerLayout)

        // Handle Navigation item clicks
        // This works with no further action on your part if the menu and destination id’s match.
        navigationView.setupWithNavController(navController)

    }

    override fun onSupportNavigateUp(): Boolean {
        // Allows NavigationUI to support proper up navigation or the drawer layout
        // drawer menu, depending on the situation
        return navController.navigateUp(drawerLayout)
    }
}

자바

public class MainActivity extends AppCompatActivity {

    private DrawerLayout drawerLayout;
    private NavController navController;
    private NavigationView navigationView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        drawerLayout = findViewById(R.id.drawer_layout);
        NavHostFragment navHostFragment = (NavHostFragment)
            getSupportFragmentManager().findFragmentById(R.id.main_content);
        navController = navHostFragment.getNavController();
        navigationView = findViewById(R.id.nav_view);

        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        // Show and Manage the Drawer and Back Icon
        NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout);

        // Handle Navigation item clicks
        // This works with no further action on your part if the menu and destination id’s match.
        NavigationUI.setupWithNavController(navigationView, navController);

    }

    @Override
    public boolean onSupportNavigateUp() {
        // Allows NavigationUI to support proper up navigation or the drawer layout
        // drawer menu, depending on the situation.
        return NavigationUI.navigateUp(navController, drawerLayout);

    }
}

BottomNavigationView 기반 탐색과 메뉴 기반 탐색 모두에서 이와 동일한 기법을 사용할 수 있습니다. 자세한 예는 NavigationUI로 UI 구성요소 업데이트를 참고하세요.

활동 대상 추가

앱의 각 화면이 탐색 구성요소를 사용하도록 연결되고 프래그먼트 기반 대상 간을 전환하는 데 더 이상 FragmentTransactions를 사용하지 않으면, 다음 단계는 startActivity 호출을 제거하는 것입니다.

먼저 앱에서 두 개의 별도 탐색 그래프가 있고 이 그래프 사이를 전환하는 데 startActivity를 사용하는 장소를 식별합니다.

이 예에는 두 그래프(A 및 B)와 A에서 B로 전환하기 위한 startActivity() 호출이 포함되어 있습니다.

Kotlin

fun navigateToProductDetails(productId: String) {
    val intent = Intent(this, ProductDetailsActivity::class.java)
    intent.putExtra(KEY_PRODUCT_ID, productId)
    startActivity(intent)
}

자바

private void navigateToProductDetails(String productId) {
    Intent intent = new Intent(this, ProductDetailsActivity.class);
    intent.putExtra(KEY_PRODUCT_ID, productId);
    startActivity(intent);

다음으로, 이러한 그래프와 호출을 그래프 A에서 그래프 B의 호스트 활동으로의 이동을 나타내는 활동 대상으로 대체합니다. 그래프 B의 시작 대상으로 전달할 인수가 있는 경우 활동 대상 정의에 이러한 인수를 지정할 수 있습니다.

다음 예에서 그래프 A는 작업과 함께 product_id 인수를 받는 활동 대상을 정의합니다. 그래프 B에는 변경사항이 포함되지 않습니다.

그래프 A와 B의 XML 표현은 다음과 같을 수 있습니다.

<!-- Graph A -->
<navigation 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/product_list_graph"
    app:startDestination="@id/product_list">

    <fragment
        android:id="@+id/product_list"
        android:name="com.example.android.persistence.ui.ProductListFragment"
        android:label="Product List"
        tools:layout="@layout/product_list_fragment">
        <action
            android:id="@+id/navigate_to_product_detail"
            app:destination="@id/product_details_activity" />
    </fragment>

    <activity
        android:id="@+id/product_details_activity"
        android:name="com.example.android.persistence.ui.ProductDetailsActivity"
        android:label="Product Details"
        tools:layout="@layout/product_details_host">

        <argument
            android:name="product_id"
            app:argType="integer" />

    </activity>

</navigation>
<!-- Graph B -->
<navigation 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"
    app:startDestination="@id/product_details">

    <fragment
        android:id="@+id/product_details"
        android:name="com.example.android.persistence.ui.ProductDetailsFragment"
        android:label="Product Details"
        tools:layout="@layout/product_details_fragment">
        <argument
            android:name="product_id"
            app:argType="integer" />
    </fragment>

</navigation>

프래그먼트 대상으로 이동하는 데 사용하는 동일한 메커니즘을 사용하여 그래프 B의 호스트 활동으로 이동할 수 있습니다.

Kotlin

fun navigateToProductDetails(productId: String) {
    val directions = ProductListDirections.navigateToProductDetail(productId)
    findNavController().navigate(directions)
}

Java

private void navigateToProductDetails(String productId) {
    ProductListDirections.NavigateToProductDetail directions =
            ProductListDirections.navigateToProductDetail(productId);
    Navigation.findNavController(getView()).navigate(directions);

시작 대상 프래그먼트에 활동 대상 인수 전달

이전 예와 마찬가지로 대상 활동이 추가 항목을 수신하는 경우 이를 시작 대상에 인수로 직접 전달할 수 있지만 아래 예에서와 같이 호스트 활동의 onCreate() 메서드 내부에 호스트의 탐색 그래프를 수동으로 설정해야 추가 인텐트를 프래그먼트에 인수로 전달할 수 있습니다.

Kotlin

class ProductDetailsActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.product_details_host)
        val navHostFragment = supportFragmentManager.findFragmentById(R.id.main_content) as NavHostFragment
        val navController = navHostFramgent.navController
        navController
                .setGraph(R.navigation.product_detail_graph, intent.extras)
    }

}

자바

public class ProductDetailsActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.product_details_host);
        NavHostFragment navHostFragment = (NavHostFragment)
            getSupportFragmentManager().findFragmentById(R.id.main_content);
        NavController navController = navHostFragment.getNavController();
        navController
                .setGraph(R.navigation.product_detail_graph, getIntent().getExtras());
    }

}

데이터는 다음 예에서와 같이 생성된 인수 클래스를 사용하여 프래그먼트 인수 Bundle에서 추출될 수 있습니다.

Kotlin

class ProductDetailsFragment : Fragment() {

    val args by navArgs<ProductDetailsArgs>()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val productId = args.productId
        ...
    }
    ...

Java

public class ProductDetailsFragment extends Fragment {

    ProductDetailsArgs args;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        args = ProductDetailsArgs.fromBundle(requireArguments());
    }

    @Override
    public void onViewCreated(@NonNull View view,
            @Nullable Bundle savedInstanceState) {
       int productId = args.getProductId();
       ...
    }
    ...

활동 결합

프래그먼트 1개가 포함된 간단한 FrameLayout과 같이 여러 활동이 동일한 레이아웃을 공유하는 경우 탐색 그래프를 결합할 수 있습니다. 대부분의 경우 각 탐색 그래프의 모든 요소를 결합하고 모든 활동 대상 요소를 프래그먼트 대상으로 업데이트하면 됩니다.

다음 예는 이전 섹션의 그래프 A와 B를 결합합니다.

결합하기 전:

<!-- Graph A -->
<navigation 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/product_list_graph"
    app:startDestination="@id/product_list">

    <fragment
        android:id="@+id/product_list"
        android:name="com.example.android.persistence.ui.ProductListFragment"
        android:label="Product List Fragment"
        tools:layout="@layout/product_list">
        <action
            android:id="@+id/navigate_to_product_detail"
            app:destination="@id/product_details_activity" />
    </fragment>
    <activity
        android:id="@+id/product_details_activity"
        android:name="com.example.android.persistence.ui.ProductDetailsActivity"
        android:label="Product Details Host"
        tools:layout="@layout/product_details_host">
        <argument android:name="product_id"
            app:argType="integer" />
    </activity>

</navigation>
<!-- Graph B -->
<navigation 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/product_detail_graph"
    app:startDestination="@id/product_details">

    <fragment
        android:id="@+id/product_details"
        android:name="com.example.android.persistence.ui.ProductDetailsFragment"
        android:label="Product Details"
        tools:layout="@layout/product_details">
        <argument
            android:name="product_id"
            app:argType="integer" />
    </fragment>
</navigation>

결합한 후:

<!-- Combined Graph A and B -->
<navigation 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/product_list_graph"
    app:startDestination="@id/product_list">

    <fragment
        android:id="@+id/product_list"
        android:name="com.example.android.persistence.ui.ProductListFragment"
        android:label="Product List Fragment"
        tools:layout="@layout/product_list">
        <action
            android:id="@+id/navigate_to_product_detail"
            app:destination="@id/product_details" />
    </fragment>

    <fragment
        android:id="@+id/product_details"
        android:name="com.example.android.persistence.ui.ProductDetailsFragment"
        android:label="Product Details"
        tools:layout="@layout/product_details">
        <argument
            android:name="product_id"
            app:argType="integer" />
    </fragment>

</navigation>

병합하는 동안 작업 이름을 동일하게 유지하면 이 프로세스가 원활해져서 기존 코드베이스를 변경할 필요가 없습니다. 예를 들어 navigateToProductDetail은 여기에서 동일하게 유지됩니다. 이제 이 작업이 활동 대상이 아닌 동일한 NavHost 내에 있는 프래그먼트 대상으로 이동함을 나타낸다는 것이 유일한 차이점입니다.

Kotlin

fun navigateToProductDetails(productId: String) {
    val directions = ProductListDirections.navigateToProductDetail(productId)
    findNavController().navigate(directions)
}

Java

private void navigateToProductDetails(String productId) {
    ProductListDirections.NavigateToProductDetail directions =
            ProductListDirections.navigateToProductDetail(productId);
    Navigation.findNavController(getView()).navigate(directions);

추가 리소스

탐색 관련 정보에 관한 자세한 내용은 다음 주제를 참고하세요.