Criar um gráfico de maneira programática usando a DSL do Kotlin

O componente de navegação fornece uma linguagem específica de domínio baseada em Kotlin, ou DSL, que depende dos builders com segurança de tipos do Kotlin. Essa API permite compor seu gráfico de maneira declarativa no código Kotlin, em vez de dentro de um recurso XML. Isso pode ser útil se você quer criar a navegação do seu app dinamicamente. Por exemplo, o app pode fazer o download e armazenar em cache uma configuração de navegação de um serviço da Web externo e usar essa configuração para criar dinamicamente um gráfico de navegação na função onCreate() da sua atividade.

Dependências

Para usar a DSL do Kotlin, adicione a seguinte dependência ao arquivo build.gradle do app:

Groovy

dependencies {
    def nav_version = "2.5.3"

    api "androidx.navigation:navigation-fragment-ktx:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.5.3"

    api("androidx.navigation:navigation-fragment-ktx:$nav_version")
}

Como criar um gráfico.

Vamos começar com um exemplo básico com base no app Sunflower. Nesse exemplo, temos dois destinos: home e plant_detail. O destino home está presente quando o usuário inicia o app pela primeira vez. Esse destino exibe uma lista de plantas do jardim do usuário. Quando o usuário seleciona uma das plantas, o app navega para o destino plant_detail.

A Figura 1 mostra esses destinos junto com os argumentos exigidos pelo destino plant_detail e uma ação, to_plant_detail, que o app usa para navegar de home para plant_detail.

O app Sunflower tem dois destinos, além de uma ação que os conecta.
Figura 1. O app Sunflower tem dois destinos, home e plant_detail, além de uma ação que os conecta.

Criar o host para o gráfico de navegação DSL do Kotlin

Independentemente de como você cria seu gráfico, será necessário hospedá-lo em umNavHost. O Sunflower usa fragmentos. Portanto, usaremos um NavHostFragmentdentro de uma FragmentContainerView, conforme mostrado no exemplo a seguir.

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

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

</FrameLayout>

Observe que o atributo app:navGraph não está definido nesse exemplo, já que o gráfico é criado programaticamente em vez de ser definido como um recurso XML.

Criar constantes para seu gráfico

Ao trabalhar com gráficos de navegação baseados em XML, o processo de compilação do Android analisa o arquivo de recurso do gráfico e define constantes numéricas para cada atributo id definido no gráfico. Essas constantes podem ser acessadas no código por meio de uma classe de recursos gerada, R.id.

Por exemplo, o snippet de gráfico XML a seguir declara um destino de fragmento com um id, home.

<navigation ...>
   <fragment android:id="@+id/home" ... />
   ...
</navigation>

O processo de compilação cria um valor constante, R.id.home, que está associado a esse destino. Em seguida, você pode referir-se a esse destino a partir do seu código, usando esse valor constante.

Esse processo de análise e geração de constantes não ocorre quando você cria um gráfico de maneira programática usando a DSL do Kotlin. Em vez disso, é necessário definir suas constantes para cada destino, ação e argumento que tenha um valor id. Cada ID precisa ser único e consistente em todas as mudanças de configuração.

Uma maneira organizada de criar constantes é criar um conjunto aninhado de objects do Kotlin que definam as constantes estaticamente, conforme mostrado no exemplo a seguir.

object nav_graph {

    const val id = 1 // graph id

    object dest {
        const val home = 2
        const val plant_detail = 3
    }

    object action {
        const val to_plant_detail = 4
    }

    object args {
        const val plant_id = "plantId"
    }
}

Com essa estrutura, é possível acessar os valores de ID no código encadeando as chamadas de objeto, conforme mostrado nos exemplos a seguir.

nav_graph.id                     // graph id
nav_graph.dest.home              // home destination id
nav_graph.action.to_plant_detail // action home -> plant_detail id
nav_graph.args.plant_id          // destination argument name

Depois de definir seu conjunto inicial de IDs, você pode criar o gráfico de navegação. Use a função de extensão NavController.createGraph() para criar um NavGraph, transmitindo um id para seu gráfico, um valor de ID para o startDestination e um lambda que define a estrutura do gráfico.

Você pode criar seu gráfico na função onCreate() da sua atividade. createGraph() retorna um Navgraph que você pode atribuir à propriedade graph do NavController associado a seu NavHost, conforme mostrado no exemplo a seguir.

class GardenActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_garden)

        val navHostFragment = supportFragmentManager
                .findFragmentById(R.id.nav_host) as NavHostFragment

        navHostFragment.navController.apply {
            graph = createGraph(nav_graph.id, nav_graph.dest.home) {
                fragment<HomeViewPagerFragment>(nav_graph.dest.home) {
                    label = getString(R.string.home_title)
                    action(nav_graph.action.to_plant_detail) {
                        destinationId = nav_graph.dest.plant_detail
                    }
                }
                fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
                    label = getString(R.string.plant_detail_title)
                    argument(nav_graph.args.plant_id) {
                        type = NavType.StringType
                    }
                }
            }
        }
    }
}

Neste exemplo, o lambda final define dois destinos de fragmento usando a função de criador DSL fragment(). Essa função requer um ID para o destino. A função também aceita um lambda opcional para configuração adicional, por exemplo, o label de destino, bem como funções incorporadas do builder para ações, argumentos e links diretos.

A classe Fragment que gerencia a IU de cada destino é transmitida como um tipo parametrizado entre colchetes (<>). Isso tem o mesmo efeito de definir o atributo android:name em destinos de fragmento definidos usando XML.

Depois de criar e definir seu gráfico, você poderá navegar de home para plant_detail usando NavController.navigate(), conforme mostrado no exemplo a seguir.

private fun navigateToPlant(plantId: String) {

    val args = bundleOf(nav_graph.args.plant_id to plantId)

    findNavController().navigate(nav_graph.action.to_plant_detail, args)
}

Tipos de destino compatíveis

A DSL do Kotlin é compatível com destinos Fragment, Activity e NavGraph, cada um com a própria função de extensão in-line disponível para criar e configurar o destino.

Destinos de fragmento

A função DSL fragment() pode ser parametrizada para a classe Fragment de implementação. Essa função usa um ID exclusivo para atribuir a esse destino, com um lambda em que você pode fornecer configurações adicionais.

fragment<FragmentDestination>(nav_graph.dest.fragment_dest_id) {
   label = getString(R.string.fragment_title)
   // arguments, actions, deepLinks...
}

Destino da atividade

A função DSL activity() usa um ID exclusivo para atribuir a esse destino, mas não é parametrizada para nenhuma classe de atividade de implementação. Em vez disso, você pode definir um activityClass opcional em um lambda final. Essa flexibilidade permite definir um destino de atividade para uma atividade iniciada a partir de uma intent implícita, em que uma classe de atividade explícita não faria sentido. Assim como nos destinos de fragmento, você também pode definir e configurar um rótulo e quaisquer argumentos.

activity(nav_graph.dest.activity_dest_id) {
    label = getString(R.string.activity_title)
    // arguments, actions, deepLinks...

    activityClass = ActivityDestination::class
}

Você pode usar a função DSL navigation() para criar um gráfico de navegação aninhado. Assim como nos outros tipos de destino, essa função DSL usa três argumentos: um ID para atribuir ao gráfico, um ID de destino inicial para o gráfico e um lambda para configurá-lo ainda mais. Os elementos válidos para o lambda incluem argumentos, ações, outros destinos, links diretos e um rótulo.

navigation(nav_graph.dest.nav_graph_dest, nav_graph.dest.start_dest) {
   // label, arguments, actions, other destinations, deep links
}

Compatibilidade com destinos personalizados

É possível usar addDestination() para adicionar tipos de destino personalizados à sua DSL do Kotlin que não são compatíveis diretamente por padrão, conforme mostrado no exemplo a seguir.

// The NavigatorProvider is retrieved from the NavController
val customDestination = navigatorProvider[CustomNavigator::class].createDestination().apply {
    id = nav_graph.dest.custom_dest_id
}
addDestination(customDestination)

Você também pode usar o operador em conjunto com o unário (+) para adicionar um destino recém-construído diretamente ao gráfico:

// The NavigatorProvider is retrieved from the NavController
+navigatorProvider[CustomNavigator::class].createDestination().apply {
    id = nav_graph.dest.custom_dest_id
}

Como fornecer argumentos de destino

Você pode definir argumentos opcionais ou obrigatórios para qualquer tipo de destino. Para definir um argumento, chame a função argument() em NavDestinationBuilder, a classe base para todos os tipos de criador de destino. Essa função usa o nome do argumento como uma String e um lambda que você pode usar para criar e configurar um NavArgument. Dentro do padrão, você pode especificar os tipos de dados do argumento, um valor padrão, se aplicável, e se o valor do argumento pode ser null.

fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
    label = getString(R.string.plant_details_title)
    argument(nav_graph.args.plant_name) {
        type = NavType.StringType
        defaultValue = getString(R.string.default_plant_name)
        nullable = true  // default false
    }
}

Se um defaultValue for fornecido, type será opcional. Nesse caso, se nenhum type for especificado, o tipo será inferido de defaultValue. Se defaultValue e type forem fornecidos, os tipos precisarão corresponder. Para ver uma lista completa de tipos de argumento, consulte NavType.

Ações

É possível definir ações em qualquer destino, incluindo ações globais no gráfico de navegação raiz. Para definir uma ação, use a função NavDestinationBuilder.action(), fornecendo um ID para a função e um lambda para fornecer configuração adicional.

O exemplo a seguir cria uma ação com um destinationId, animações de transição e comportamento de pop e de modo superior único.

action(nav_graph.action.to_plant_detail) {
    destinationId = nav_graph.dest.plant_detail
    navOptions {
        anim {
            enter = R.anim.nav_default_enter_anim
            exit = R.anim.nav_default_exit_anim
            popEnter = R.anim.nav_default_pop_enter_anim
            popExit = R.anim.nav_default_pop_exit_anim
        }
        popUpTo(nav_graph.dest.start_dest) {
            inclusive = true // default false
        }
        // if popping exclusively, you can specify popUpTo as
        // a property. e.g. popUpTo = nav_graph.dest.start_dest
        launchSingleTop = true // default false
    }
}

Links diretos

Você pode adicionar links diretos a qualquer destino, assim como em um gráfico de navegação baseado em XML. Os mesmos procedimentos definidos em Como criar um link direto para um destino se aplicam ao processo de criação de um link direto explícito usando a DSL do Kotlin.

No entanto, ao criar um link direto implícito, você não tem um recurso de navegação XML que pode ser analisado para elementos <deepLink>. Portanto, não é possível confiar na colocação de um elemento <nav-graph> no arquivo AndroidManifest.xml. Em vez disso, é necessário adicionar filtros de intent manualmente à atividade. O filtro de intent fornecido deve corresponder ao padrão de URL base dos links diretos do seu app.

Para cada destino com link direto individual, você pode fornecer um padrão de URI mais específico, usando a função DSL deepLink(). Essa função aceita uma String para o padrão de URI, conforme mostrado no exemplo a seguir.

deepLink("http://www.example.com/plants/")

Não há limite para o número de URIs de link direto que você pode adicionar. Cada chamada para deepLink() anexa um novo link direto a uma lista interna específica para esse destino.

Veja um cenário de link direto implícito mais complexo que também define parâmetros baseados em caminho e consulta:

val baseUri = "http://www.example.com/plants"

fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
    label = getString(R.string.plant_details_title)
    deepLink("${baseUri}/{id}")
    deepLink("${baseUri}/{id}?name={plant_name}")
    argument(nav_graph.args.plant_id) {
       type = NavType.IntType
    }
    argument(nav_graph.args.plant_name) {
        type = NavType.StringType
        nullable = true
    }
}

A interpolação de strings pode ser usada para simplificar a definição.

Como criar IDs

A biblioteca de navegação exige que os valores de IDs usados para elementos do gráfico sejam números inteiros únicos que permaneçam constantes por meio de alterações de configuração. Uma maneira de criar esses IDs é defini-los como constantes estáticas, conforme mostrado em Criar constantes para o gráfico. Também é possível definir IDs de recursos estáticos em XML como um recurso. Como alternativa, você pode criar IDs dinamicamente. Por exemplo, você pode criar um contador de sequência que seja incrementado sempre que referir-se a ele.

object nav_graph {
    // Counter for id's. First ID will be 1.
    var id_counter = 1

    val id = id_counter++

    object dest {
       val home = id_counter++
       val plant_detail = id_counter++
    }

    object action {
       val to_plant_detail = id_counter++
    }

    object args {
       const val plant_id = "plantId"
    }
}

Limitações

  • O plug-in Safe Args é incompatível com a DSL do Kotlin, porque o plug-in procura arquivos de recursos XML para gerar classes Directions e Arguments.