ナビゲーション コンポーネントに移行する

コレクションでコンテンツを整理 必要に応じて、コンテンツの保存と分類を行います。

Navigation コンポーネントは、複雑なナビゲーションや、遷移アニメーション、ディープリンク、アプリの画面間で渡されるコンパイル時チェック引数を管理できるライブラリです。

このドキュメントでは、Navigation コンポーネントを使用するように既存のアプリを移行する一般的な方法について説明します。

移行は、大きく分けて以下のステップで構成されます。

  1. 画面固有の UI ロジックをアクティビティから移動する - アプリの UI ロジックをアクティビティから移動して、各アクティビティには Toolbar などのグローバル ナビゲーション UI コンポーネントのロジックだけが含まれるようにし、各画面の実装はフラグメントやカスタム デスティネーションに委任します。

  2. Navigation コンポーネントを統合する - アクティビティごとに、そのアクティビティが管理する 1 つまたは複数のフラグメントを組み込んだナビゲーション グラフを作成します。フラグメント トランザクションを Navigation コンポーネント処理に置き換えます。

  3. アクティビティ デスティネーションを追加する - startActivity() 呼び出しを、アクティビティ デスティネーションを使用するアクションに置き換えます。

  4. アクティビティを結合する - 複数のアクティビティが共通のレイアウトを共有している場合、ナビゲーション グラフを結合します。

要件

このガイドでは、すでに AndroidX ライブラリを使用するようアプリを移行済みであることを前提としています。この移行をまだ行っていない場合は、AndroidX を使用するようにプロジェクトを移行してから、下記の手順に進んでください。

画面固有の UI ロジックをアクティビティから移動する

アクティビティは、アプリと Android との間のグラフィカル インタラクションを促進するシステムレベル コンポーネントです。アクティビティは、アプリのマニフェスト内に登録します。これにより、Android は起動可能なアクティビティを認識します。アクティビティ クラスを使用すると、アプリ側も Android の変化(アプリ UI のフォアグラウンド化 / バックグラウンド化、回転など)に対応できるようになります。また、アクティビティは、画面間で状態を共有する場所としても機能します。

アプリのコンテキスト内において、アクティビティは、ナビゲーションのホストとして機能し、画面間の遷移方法やデータの受け渡し方法などに関するロジックと情報を保持する必要があります。ただし、UI の詳細を管理する機能は、UI 内の再利用可能な小さなパーツに任せる方が便利です。このパターンに関して推奨される実装は、フラグメントです。フラグメントを利用するメリットの詳細については、単一アクティビティ: 移行する理由、タイミング、方法をご覧ください。ナビゲーションは、ナビゲーションとフラグメントの依存関係を通じてフラグメントをサポートします。また、ナビゲーションは、カスタム デスティネーション タイプもサポートしています。

アプリがフラグメントを使用していない場合は、まず、フラグメントを使用するようにアプリ内の各画面を移行する必要があります。この段階では、まだアクティビティは削除しません。画面を表現するフラグメントを作成して、役割ごとに UI ロジックを分割します。

フラグメントを導入する

2 つの画面(アイテムリスト画面とアイテム情報画面)で構成されるアプリを例として、フラグメントを導入するプロセスについて説明します。このアプリでは、リスト画面でアイテムをタップすると、そのアイテムの詳細情報画面が表示されます。

この例では、現在のところ、リスト画面と詳細情報画面はそれぞれ個別のアクティビティになっています。

新しいレイアウトを作成して UI をホストする

フラグメントを導入するには、まず、フラグメントをホストするアクティビティ用の新しいレイアウト ファイルを作成します。この新しいファイルが、アクティビティの現在のコンテンツ ビュー レイアウトに取って代わります。

シンプルなビューの場合、FrameLayout を使用できます。下記の product_list_host サンプルをご覧ください。

<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() 関数内のレイアウト ファイル参照を編集して、この新しいレイアウト ファイルを指定します。

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)
        ...
    }
}

Java

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
    }
   ...
}

Java

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)
    }

Java

// 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)
        }
    }
    ...
}

Java

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)
    }
}

Java

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()
    }
}

Java

@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()
}

...

Java

...

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 コンポーネントを統合する

フラグメント ベース アーキテクチャを使用するようになったら、Navigation コンポーネントの統合を開始できます。

まず、Navigation ライブラリ リリースノートに記載された手順に沿って、最新の Navigation 依存関係をプロジェクトに追加します。

ナビゲーション グラフを作成する

Navigation コンポーネントは、アプリのビュー表示と同様に、リソース ファイル内のアプリのナビゲーション構成をグラフとして表現します。これにより、コードベースの外部でアプリのナビゲーションを簡単に整理し、視覚的に編集できるようになります。

ナビゲーション グラフを作成するには、まず「navigation」という名前の新しいリソース フォルダを作成します。グラフを追加するには、このディレクトリを右クリックして、[New] > [Navigation resource file] を選択します。

Navigation コンポーネントは、アクティビティをナビゲーションのホストとして使用し、ユーザーがアプリ内を移動する際、各フラグメントをそのホストにスワップします。アプリのナビゲーションを視覚的にレイアウトするには、このグラフをホストするアクティビティ内に NavHost を設定する必要があります。フラグメントを使用しているため、Navigation コンポーネントのデフォルト 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 が、システムの [戻る] ボタンをインターセプトできるようになります。

DrawerLayoutBottomNavigationView といったトップレベル ナビゲーションを使用している場合、この 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>

下部にある [Design] タブをクリックすると、下記のようなグラフが表示されます。グラフの左上にある [Destinations] の下に、NavHost アクティビティへの参照が layout_name (resource_id) 形式で表示されています。

このグラフにフラグメントを追加するには、上部にあるプラスボタン()をクリックします。

Navigation コンポーネントでは、各画面のことを「デスティネーション」と呼びます。 デスティネーションとして指定できるのは、フラグメント、アクティビティ、カスタム デスティネーションです。どのタイプのデスティネーションでもグラフに追加できますが、アクティビティ デスティネーションは「ターミナル デスティネーション」と見なされます。アクティビティ デスティネーションに移動すると、別個のナビゲーション ホストおよびナビゲーション グラフの枠内に移ることになります。

Navigation コンポーネントでは、ユーザーがデスティネーション間を移動する仕組みのことを「アクション」と呼びます。アクションでは、遷移アニメーションやポップ動作を表現することもできます。

フラグメント トランザクションを削除する

Navigation コンポーネントを使用している場合、同一のアクティビティの下でフラグメント ベース画面間を移動するのであれば、FragmentManager インタラクションを削除できます。

同一のアクティビティまたはトップレベル ナビゲーション(ドロワー レイアウトやボトム ナビゲーション)の下で複数のフラグメントを使用するアプリの場合、これまでは、UI のメイン コンテンツ セクション内でフラグメントの追加や置換を行うには、通常 FragmentManagerFragmentTransactions を使用していました。今後は、Navigation コンポーネントを使用する方法に置き換えられ、グラフ内でアクションとデスティネーションをリンクさせて、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()
    }
}

Java

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)
    }
}

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 -> (
        ((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)
    }
}

Java

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 を更新します。

引数を安全に渡す

Navigation コンポーネントには、Safe Args と呼ばれる Gradle プラグインが含まれています。Safe Args は、デスティネーションとアクション用に指定された引数に対してタイプセーフにアクセスするためのシンプルなオブジェクトとビルダークラスを生成します。

このプラグインを適用し、ナビゲーション グラフ内のデスティネーションに対して引数を定義すると、Navigation コンポーネント フレームワークが、ターゲット デスティネーションへのタイプセーフな引数を提供する Arguments クラスを生成します。アクションを定義すると、このプラグインは Directions 構成クラスを生成します。この構成クラスを使用することで、ユーザーをターゲット デスティネーションに移動させる方法を NavController に伝えることができます。引数を必要とするデスティネーションをアクションがポイントしていた場合、生成される 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)
}

Java

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);
    }
}

Navigation コンポーネントをプロジェクトに追加してナビゲーション グラフを作成したら、グラフから各コンテンツ デスティネーション(上記の例の場合、「Home」、「Gallery」、「SlideShow」、「Tools」)を追加します。メニュー項目の 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 を接続することで、メニュー項目に基づいて自動的にナビゲーションを処理できるようになります。また、NavController により、DrawerLayout の開閉や、[上へ] ボタンの動作、[戻る] ボタンの動作を適切に処理できます。

そして、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)
    }
}

Java

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);
        navController = Navigation.findNavController(this, R.id.content_main);
        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 コンポーネントを更新するをご覧ください。

アクティビティ デスティネーションを追加する

Navigation コンポーネントを使用するようにアプリ内の各画面を設定し、FragmentTransactions によるフラグメント ベース デスティネーション間の遷移を削除したら、次のステップとして、startActivity 呼び出しを削除します。

まず、2 つのナビゲーション グラフと、startActivity を使用してグラフ間を遷移しているアプリ内の場所を見つけます。

この例には、2 つのグラフ(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)
}

Java

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)
        findNavController(R.id.main_content)
                .setGraph(R.navigation.product_detail_graph, intent.extras)
    }

}

Java

public class ProductDetailsActivity extends AppCompatActivity {

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

        setContentView(R.layout.product_details_host);
        findNavController(R.id.main_content)
                .setGraph(R.navigation.product_detail_graph, getIntent().getExtras());
    }

}

生成された args クラスを使用して、フラグメント引数 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();
       ...
    }
    ...

アクティビティを結合する

単一のフラグメントで構成されるシンプルな 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);

参考情報

ナビゲーション関連の詳細については、以下のトピックをご覧ください。