Mostrando conteúdo rápido no Android: App Actions & Slices
Tags: android, assistant, androiddev
Published: 2020-04-27 20:22:04

Atualmente está cada vez mais frequente o uso de assistentes virtuais, principalmente em smartphones. Eu, por exemplo, sempre apago a luz do meu quarto por comando de voz, dentre outras inúmeras possibilidades. Neste cenário, os aplicativos Android conseguem obter ainda mais engajamento dos usuários através de respostas rápidas de seus conteúdos integrando com o Google Assistant. Para isso, a Google lançou o **App Actions**.

Antes de continuar, é necessário pontuar que essa ferramenta, no dia da publicação deste artigo, se encontra em Developer Preview e tem algumas limitações: apenas um conjunto limitado de ações está disponível e para algumas linguagens específicas (que varia de acordo com a ação). Veja a lista de ações reconhecidas (_Built-in intents_) [aqui](https://developers.google.com/assistant/app/reference/built-in-intents).

## Como funciona

Na percepção do usuário, uma **App Action** é como se fosse um “atalho” do _Google Assistant_ para o app. Na realidade, o que acontece é que o _Assistant_ faz o reconhecimento da requisição do usuário e interpreta esses dados como um _Intent_ (necessariamente precisam ser aqueles _Built-in_, conforme mencionado anteriormente) com seus _fullfilments_ (url, parâmetros, etc.) relacionados. Após a interpretação, é feito um mapeamento com as ações correspondentes aos _Intents_ que estiverem registrados no arquivo de ações (`actions.xml`) do app.

Ao identificar um _Intent_ relacionado a uma(s) ação(ões), o _Assistant_ interpretra os parâmetros como uma entidade - que deve seguir o padrão [schema.org](https://schema.org/) - e também a url (_deep link_) mapeado no arquivo de ações do app. Por fim o sistema identifica e direciona o usuário direto para o conteúdo do app. No caso do uso de _Slice_, a informação é mostrado diretamente no _Assistant_ (Veremos melhor mais pra frente).

| Comando no _assistant_ | Resultado no App |
| --- | --- |
| ![](https://miro.medium.com/max/512/1*-IJfzBx8MrW-fqTfHwIxUQ.png) | ![](https://miro.medium.com/max/512/1*PBBSbysMyWjT_d2huzO9Jw.png) |
<figcaption>Exemplo de funcionamento do App Actions</figcaption>

## Projeto

Como exemplo de código para este artigo, criei um projeto de assistente de receitas, em que você pergunta ao _Google Assistant_ coisas como “Olá Google, abrir receita de Pão de queijo” e assim é mostrado um _slice_ do app diretamente no _assistant_. Para dispositivos em que os slices não estiverem disponíveis é apresentado uma _Activity_ com os detalhes da receita.

Os dados das receitas foram alimentados por um json presente no [Gist](https://gist.github.com/lucasheriques/ed2214dba65b8903a5b62566f4439005). Para facilitar um eventual escalonamento nos dados, pensando numa possibilidade de adicionar novas receitas futuramente, importei esse json para um banco de dados no [Firestore](https://firebase.google.com/docs/firestore) do [Firebase](https://firebase.google.com/).

Resumindo: o app recebe o nome da receita como parâmetro do _assistant_, faz a busca no _Firestore_ e, assim que encontrado, mostra ao usuário o conteúdo.

## Configurações iniciais

Antes de começar, precisamos configurar o ambiente para conseguir testar as ações que implementarmos.

### Criar um projeto na Playstore

Para que as ações sejam reconhecidas pelo _Google Assistant_ (mesmo em teste) é necessário criar um projeto na _Playstore_ e subir pelo menos um build (APK ou aab) com o arquivo das ações do app registrado no _Manifest_. Não é necessário publicar o app, basta ter um artefato salvo.

Para mais informações sobre como inserir um app na Playstore, veja na [documentação](https://support.google.com/googleplay/android-developer/answer/113469).

### Instalar plugin para testar as ações

Para rodar os testes, antes de publicar o app, é necessário instalar o plugin **App Actions Test Tool** no _Android Studio_. Para saber como instalar veja [aqui](https://developers.google.com/assistant/app/test-tool#getting_the_tool). Essa ferramenta permite configurar os parâmetros da ação a ser testada.

É importante pontuar que esse plugin tem algumas restrições:

- Funciona apenas com dispositivos físicos
- Suporta apenas algumas localidades (_en-US, en-GB, en-CA, en-IN, en-BE, en-SG, e en-AU_). Com exceção de alguns _built-in intents_ que aceitam algumas outras localidades, mas ainda bem limitado.

Após instalado, também é necessário fazer o login no _Android Studio_ com a mesma conta do dispositivo que você irá usar para os testes.

![App Actions Test Tool](https://miro.medium.com/max/1400/1*Fyk2dS-B0ZyOnHfzyTiILw.png)
<figcaption>App Actions Test Tool - Página de instalação do plugin no Android Studio.</figcaption>

### Adicionar suporte a deep links no app

Por fim, é necessário configurar quais telas do app devem tratar os _deep links_ transferidos do _assistant_. No caso do projeto, foi configurado apenas que o link `recipes://detail` abre a tela de detalhe da receita:

```xml
<!-- AndroidManifest.xml -->
<activity android:name=".RecipeDetailActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data
            android:host="detail"
            android:scheme="recipes" />
    </intent-filter>
</activity>
```

Para adicionar suporte a _deep link_, basta adicionar dentro da tag `<activity>` desejada no manifest as configurações de `<intent-filter>` com `<action>` do tipo `android.intent.action.VIEW` e com as categorias `android.intent.category.DEFAULT` e `android.intent.category.BROWSABLE` (essa última é bem importante para o funcionamento das ações). Além disso é necessário definir o `schema` (protocolo) e o `host` da url que abrirá essa activity. É possível adicionar também definições de parâmetros esperados e _path_ da url, mas não é necessário no momento. Para mais informações sobre _deep links_ em Android, veja na [documentação](https://developer.android.com/training/app-links/deep-linking).

## Configurando as ações

Com todas as configurações preliminares finalizadas, vamos inserir nossa primeira ação. O primeiro passo é adicionar o arquivo `actions.xml` dentro da pasta `res/xml`. É possível criar esse arquivo de forma automática no _Android Studio_: `Arquivo > Novo > XML > Arquivo XML App Actions`. Em seguida, determinar as ações possíveis do app nesse local:

```xml
<!-- actions.xml -->
<actions>
    <action intentName="actions.intent.OPEN_APP_FEATURE">
        <fulfillment urlTemplate="recipes://detail{?recipe}">
            <parameter-mapping
                intentParameter="feature"
                urlParameter="recipe" />
        </fulfillment>
    </action>
</actions>
```

Inicialmente, configurei dessa forma a ação do app. O primeiro ponto é relacionado ao `intentName` que precisa de ser um daqueles [Built-in intents](https://developers.google.com/assistant/app/reference/built-in-intents) já mencionados anteriormente. No caso deste app de receitas não existe um _intent_ pré-definido de “ver receita” então usei o genérico que é “abrir funcionalidade” (`actions.intent.OPEN_APP_FEATURE`). Na documentação desta ação, precisamos obter apenas um parâmetro do _intent_, que é o _feature_. Neste caso, os parâmetros acabam se resumindo em apenas uma _string_, mas poderia ser qualquer estrutura de dados desde de que se encaixe no padrão [schema.org](https://schema.org). Por exemplo, outros _intents_ exigem uma estrutura fixa, como por exemplo `actions.intent.ORDER_MENU_ITEM` que exige que os parâmetros sejam correspondentes ao schema de [MenuItem](https://schema.org/MenuItem).

Para facilitar a obtenção das receitas no _Firestore_, configurei um padrão para o parâmetro de `feature` em que o texto falado pelo usuário é mapeado para o id do documento no _Firestore_ relativo a receita escolhida.

```xml
<!-- actions.xml -->
<actions>
    <action intentName="actions.intent.OPEN_APP_FEATURE">
        <fulfillment urlTemplate="recipes://detail{?recipe}">
            <parameter-mapping
                intentParameter="feature"
                urlParameter="recipe" />
        </fulfillment>

        <parameter name="feature">
            <entity-set-reference entitySetId="RecipeEntitySet" />
        </parameter>
    </action>

    <entity-set entitySetId="RecipeEntitySet">
        <entity
            name="@string/frozen_yogurt"
            alternateName="@array/frozen_yogurt"
            identifier="2gbkANgwMJYMUnYgoRHn" />
        <entity
            name="@string/cheese_bread"
            alternateName="@array/cheese_bread"
            identifier="6aRoFUbVuMp3EP48uO8M" />
        <entity
            name="@string/mac_and_cheese"
            alternateName="@array/mac_and_cheese"
            identifier="7BnVOHJMxqHzUNxnYY5D" />
        ...
    </entity-set>
</actions>
```

Agora o parâmetro `feature` está configurado para aceitar qualquer valor dentro do conjunto `RecipeEntitySet`. Repare que cada `<entry>` possui o `name` que é um nome principal que o _assistant_ vai identificar a entidade; também tem `alternateName` que são sinônimos ou nomes alternativos da entidade, por exemplo: pão de queijo, biscoito de queijo, etc. O `identifier` é o identificador do documento desta entidade no _Firestore_. Ao fazer o mapeamento dos parâmetros com as entidades suportadas, o _Assistant_ inicia o _intent_ substituindo o `identifier` no lugar da `feature` e aí cabe ao app fazer o tratamento da informação.

Existem diversas possibilidades de configurações de parâmetros e entidades. Por exemplo, no lugar de um `identifier`, é possível configurar uma url para cada entidade, e aí dentro de `fullfilment` não será necessário tratar o `parameter-mapping` pois já foi feito na entidade.

Após configurar o arquivo `actions.xml` é necessário registrá-lo no `AndroidManifest.xml`:

```xml
<!-- AndroidManifest.xml -->
<application ...>
  ...
  <meta-data
    android:name="com.google.android.actions"
    android:resource="@xml/actions" />
</application>
```

### Testando as ações

Para testar as novas ações, basta inicial o plugin do **App Actions Test Tool** que foi instalado anteriormente. Ao iniciar, será pedido um nome e uma localidade para criar um preview. Ambos os campos são opcionais. Lembrando que a localidade precisa ser uma daquelas já mencionadas no início do artigo.

![App Actions Test Tool - Start](https://miro.medium.com/max/1312/1*mM0BaNgkmPkR41LEopBbow.png)
<figcaption>App Actions Test Tool — Create preview
</figcaption>

Após criar o _preview_, a ferramenta já obtém todas as ações configuradas do app, que no nosso caso agora é apenas `actions.intent.OPEN_APP_FEATURE`. No campo feature vamos inserir o nome da “funcionalidade” que ficou configurado como sendo o nome da receita. Repare que a localidade precisa ser em inglês, então coloquei como feature “cheese bread”.

| App Actions Test Tool Preview | Resultado no App |
| --- | --- |
| ![](https://miro.medium.com/max/1024/1*kgSzaqYxGXrZinbeT0XsPA.png) | ![](https://miro.medium.com/max/512/1*PBBSbysMyWjT_d2huzO9Jw.png) |
<figcaption>Rodando uma ação através do Android App Action Test Tool</figcaption>

Ao clicar em `run` o plugin inicia o _assistant_ do dispositivo com o _intent_ de abrir funcionalidade e com o parâmetro `recipe=<id_recipe>` pois foi o mapeamento que ele fez com as configurações que colocamos no _preview_. Assim que é recebido pelo dispositivo, ele já reconhece que existe o app responsável por tratar essa ação e abre diretamente a tela de detalhe da receita.

* * *

# Android Slices

Aproveitando que já fizemos uma forma rápida do usuário acessar as funcionalidades do app através das ações, porque não mostrar o conteúdo de forma mais direta e simples? Para isso, precisamos configurar um **Android Slice**. Que é uma forma dinâmica e interativa de mostrar o conteúdo do app diretamente no _assistant_, sem a necessidade de abrir o app efetivamente. Também é possível tomar algumas ações rápidas sem estar dentro de todo o contexto da aplicação.

Existem diversos templates de _slices_ que podem ser usados conforme a necessidade do aplicativo. Além disso também é possível inserir dados de forma dinâmica através de uma conexão com servidor, controladores de entrada do usuário, _live data_, e outras diversas funcionalidades. Para saber mais sobre os _Slices_ do Android, veja na [documentação](https://developer.android.com/guide/slices).

## Instalar visualizador de slices

Antes de começar a criar os slices precisamos baixar um app que vai nos permitir visualizar esses componentes.

Primeiro, é preciso baixar o app na página de releases do [_Slice Viewer_](https://github.com/android/user-interface-samples/releases). Em seguida, basta instalar o APK no dispositivo. Se preferir use diretamente o comando do adb: `adb install -r -t slice-viewer.apk`

## Criar provedor de slices

O próximo passo agora é criar um `SliceProvider` que pode ser feito diretamente pelo _Android Studio_: `Arquivo > Novo > Outros > Slice Provider`. Lembrando também que é necessário registrar esse provedor no `AndroidManifest.xml`:

```xml
<!-- AndroidManifest.xml -->
<application ...>
 ...
 <provider
     android:name=".app.RecipeSliceProvider"
     android:authorities="${applicationId}"
     android:exported="true">
     <intent-filter>
         <action android:name="android.intent.action.VIEW" />

         <category android:name="android.app.slice.category.SLICE" />

         <data
             android:host="${applicationId}"
             android:pathPattern="/detail"
             android:scheme="content" />
     </intent-filter>
 </provider>
</application>
```

No caso, o `RecipeSliceProvider` possui apenas um atributo que é uma lista mutável de receitas e, a cada nova ação acionada pelo usuário que mostra o _assistant_, é feito uma busca no _Firestore_ com o id obtido na uri e, quando retornado sucesso é mostrado um conteúdo resumido da receita no _slice_ vinculado. A decisão de usar uma lista é porque podem existir inúmeros _slices_ aparecendo juntos em uma sessão do _assistant_. Por isso ficamos com diversas referências que são associadas por seus identificadores de forma que cada _slice_ mostra o conteúdo correto da receita que corresponde.

```kotlin
/ **RecipeSliceProvider.kt** /
class RecipeSliceProvider : SliceProvider() {

    override fun onCreateSliceProvider() = true

    override fun onMapIntentToUri(intent: Intent?): Uri {
        var uriBuilder: Uri.Builder = Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
        if (intent == null) return uriBuilder.build()
        val data = intent.data
        val dataPath = data?.path
        if (data != null && dataPath != null) uriBuilder = uriBuilder.path(dataPath)
        context ?: return uriBuilder.build()
        uriBuilder = uriBuilder.authority(context!!.packageName)

        return uriBuilder.build()
    }
    
    override fun onBindSlice(sliceUri: Uri): Slice? {
        val context = context ?: return null
        return if (sliceUri.path == "/") {
          //TODO("Return slice with content")

        } else {
          //TODO("Return slice with not found error")
        }
    }
    
    override fun onSlicePinned(sliceUri: Uri?) {
      //TODO("get dynamic initial data, subscribe to any observers")
    }

    override fun onSliceUnpinned(sliceUri: Uri?) {
        super.onSliceUnpinned(sliceUri)
        //TODO("tear down every observer and remove unecessary resources")
    }
}
```

Neste provedor temos alguns pontos de atenção: `onCreateSliceProvider` é usado para iniciar qualquer objeto que seja necessário e retornar `true` caso o provedor tenha sido criado com sucesso, caso contrário, retorna `false`.

A implementação `deonMapIntentToUri` é necessária apenas no caso em que o app precisa obter e mapear as requisições de url e converter urls comuns em content uri. No caso desse app, o mapeamento do _slice_ foi feito diretamente via um _content uri_, então não foi necessária toda essa implementação.

O método `onBindSlice` é responsável por construir o _slice_ e vincular os dados necessários. Também é neste método que fazemos o mapeamento do conteúdo, através do `path` do `sliceUri`.

```kotlin
/ **RecipeSliceProvider.kt** /
override fun onBindSlice(sliceUri: Uri): Slice? {
    val context = context ?: return null
    val id = sliceUri.getQueryParameter("recipe")
    val resource = getResource(id)
    return if (sliceUri.path == "/detail") {
        when (resource) {
            is Resource.Success -> createContentSlice(context, sliceUri, resource.data)
            is Resource.Error -> createErrorSlice(context, sliceUri)
            else -> createLoadingSlice(context, sliceUri)
        }

    } else {
        createErrorSlice(context, sliceUri)
    }
}
```

Em `onSlicePinned` o _slice_ foi atrelado a um serviço e está prestes a ser exibido pro usuário. Neste momento é hora de buscar os dados do servidor, sobrescrever para algum _observable_, ou ações semelhantes, quando necessário. Ao final, quando obter os dados necessários, basta chamar `context.contentResolver.notifyChanged(sliceUri, null)` para que seja atualizado o `onBindSlice` com o novo conteúdo.

```kotlin
/ **RecipeSliceProvider.kt** /
override fun onSlicePinned(sliceUri: Uri?) {
    val id = sliceUri.getQueryParameter("recipe")
    FirestoreManager.getRecipeById(id) { task ->
        if (task.isSuccessful && task.result?.exists() == true) {
            val doc = task.result
            recipes.add(Resource.Success(doc?.data?.toRecipe(doc.id)))
            context?.contentResolver?.notifyChange(sliceUri, null)
        } else {
            recipes.add(Resource.Error(id))
            task.exception?.printStackTrace()
            context?.contentResolver?.notifyChange(sliceUri, null)
        }
    }
}
```

Para o caso exclusivo deste app e para facilitar o desenvolvimento, foi usado o SDK puro do Firestore para obtenção dos dados, de forma que foram usados apenas callbacks sem interação com _live data_, e _observables_ por exemplo.

Por fim, no `onSliceUnpinned` é a hora de remover os _observers_ e fazer qualquer limpeza para evitar vazamentos de memória. No caso do app de receitas, é feito apenas a limpeza dos dados dentro da lista `recipes`.

### Usar templates para slices

Os _Slices_ são construídos através de um sistema de lista (`ListBuilder`) que pode ter diferentes tipos de formato de linhas. O primeiro é o cabeçalho (`HeaderBuilder`) que possuem as informações principais do _slice_ e também é o conteúdo mostrado quando o _slice_ está no modo de visualização compacto. Outro formato é a linha em si (`RowBuilder`) para um conteúdo mais simples, e podendo ter uma ou mais ações rápidas atreladas. De forma semelhante tem o formato de grid (`GridBuilder`) que mostra o conteúdo em diferentes colunas dentro da mesma linha. Também é possível adicionar uma barra de progresso ou um _slider_ através do `RangeBuilder`. Para mais informações sobre templates dos _slices_ veja na [documentação](https://developer.android.com/guide/slices/templates).

Para o app de receitas usei o `HeaderBuilder` para mostrar o nome da receita no modo reduzido do _slice_ e nas outras partes, foi usado o `RowBuilder` por ser um conteúdo mais simples e único, precisando apenas de linhas mesmo. Contudo, foi configurado três tipos de _slices_: um para ser exibido enquanto os dados são carregados, outro para quando a receita foi retornada e outro para o caso de ter retornado algum erro ou não ter sido encontrada a receita.

```kotlin
/ **RecipeSliceProvider.kt** /
private fun createLoadingSlice(context: Context, sliceUri: Uri) =
  list(context, sliceUri, ListBuilder.INFINITY) {
      header {
          setTitle(context.getString(R.string.loading_recipe), true)
      }
}
```

Para os _slices_ é importante que apareça alguma informação instantânea para o usuário mesmo ainda não tendo todos os dados a serem mostrados .. por isso, foi criado o _slice_ para mostrar que o conteúdo está carregando.

```kotlin
/ **RecipeSliceProvider.kt** /
private fun createContentSlice(context: Context, sliceUri: Uri, recipe: Recipe?) =
  if (recipe != null)
    list(context, sliceUri, ListBuilder.INFINITY) {
      header {
          setTitle(recipe.name.orEmpty(), false)
          subtitle = recipe.baseIngredients?.joinToString().orEmpty()
          summary = recipe.baseIngredients?.joinToString().orEmpty()
          primaryAction = createDetailAction(recipe)
      }
      gridRow {
          cell {
              val bitmap = Glide.with(context)
                  .asBitmap()
                  .load(recipe.image)
                  .submit()
                  .get()
              val image =
                  IconCompat.createWithBitmap(bitmap)
              addImage(image, ListBuilder.LARGE_IMAGE)
          }
      }
      row {
          title = context.getString(R.string.directions)
          subtitle = recipe.directions.orEmpty()
      }
  } else createErrorSlice(context, sliceUri)
```

Para o carregamento da imagem da receita foi utilizada a biblioteca [Glide](https://bumptech.github.io/glide/) que transforma uma url de imagem em um _bitmap_ no android e, com isso foi gerado o `IconImage` necessário para ser inserido no _slice_.

```kotlin
/ **RecipeSliceProvider.kt** /
private fun createErrorSlice(context: Context, sliceUri: Uri) =
  list(context, sliceUri, ListBuilder.INFINITY) {
      header {
          title = context.getString(R.string.error_recipe_not_found)
          primaryAction = createAppAction()
      }
  }
```

Por fim, repare que em diversos momentos foi configurado também uma `primaryAction` que é uma `SliceAction` na qual possui informações de _intents_ e _pending intents_.

```kotlin
/ **RecipeSliceProvider.kt** /
private fun createDetailAction(recipe: Recipe?) =
  createAction(RecipeDetailActivity.intent(context, recipe))

private fun createAppAction() = 
  createAction(Intent(context, MainActivity::class.java))

private fun createAction(intent: Intent?): SliceAction =
  SliceAction.create(
      PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT),
      IconCompat.createWithResource(context, R.drawable.ic_launcher_foreground),
      ListBuilder.ICON_IMAGE,
      context?.getString(R.string.open_app).orEmpty()
  )
```

### Testar slice no dispositivo

Para testar no _Slice Viewer_ instalado anteriormente, é necessário criar uma nova configuração de execução no _Android Studio_.

![Slice run configuration](https://miro.medium.com/max/1400/1*zpYICuYOe377LzW4zVCRNA.png)
<figcaption>Adicionar nova configuração de execução para mostrar o Slice no Slice Viewer</figcaption>

Nomeie-a como preferir e em nas opções de lançamento (_Launch Options_) selecione o modo URL e na url coloque `slice-content://<application_id>/path` de acordo com o que você configurou no _SliceProvider_ dentro do `AndroidManifest.xml.` No caso do app de receitas, também foi necessário adicionar o parâmetro `recipe` porque precisamos dele para saber qual a receita mostrar pro usuário. Após adicionado, basta rodar essa nova configuração.

| _Slice_ carregando | _Slice_ de conteúdo | _Slice_ de erro |
| --- | --- | --- |
| ![](https://miro.medium.com/max/512/1*aq3VzEyFM2tVQIUluVKnqw.png) | ![](https://miro.medium.com/max/512/1*-8ssSMJNRoIGZsJvTmyUzQ.png) | ![](https://miro.medium.com/max/512/1*YFuauITYBabEFO163WRJNg.png) |
<figcaption>Slice Viewer — Exemplos de slice</figcaption>

## Configurar ação para usar o slice

Finalmente, vamos configurar o arquivo `actions.xml` para mostrar o _slice_ criado. Para isso, basta adicionar um novo `fullfiment` dentro da ação com `fullfilmentMode="actions.fullfilment.SLICE` e o `urlTemplate` de acordo com o que ficou no `AndroidManifest`.

```xml
<!-- actions.xml -->
<actions>
    <action intentName="actions.intent.OPEN_APP_FEATURE">
        <fulfillment
            fulfillmentMode="actions.fulfillment.SLICE"
            urlTemplate="content://com.anacoimbra.android.recipes/detail{?recipe}">
            <parameter-mapping
                intentParameter="feature"
                required="true"
                urlParameter="recipe" />
        </fulfillment>
      ...
  </action>
  ...
</actions>
```

Pronto! Agora já está configurado para abrir o _slice_ direto no _Google Assistant_. Para testar essa parte agora, basta abrir o **Android App Actions Test Tool** novamente e, se tiver um _preview_ já criado, clique em _Update Preview_ para que a ferramenta possa atualizar suas informações com as alterações feitas em `actions.xml`.

![Slice on assistant](https://miro.medium.com/max/400/1*Sb4x8oFlG7y0yaI91o3gKQ.png)
<figcaption>Slice no Google Assistant</figcaption>

* * *

## Considerações finais

Implementar ações e _slices_ para o app pode ser algo bem interessante que ajuda muito no engajamento e na interação do usuário. Contudo, muitas coisas ainda estão em fase experimental, por isso algumas questões ainda podem se alterar e tem muito a ser aprimorado.

De qualquer forma, é uma funcionalidade incrível que vale a pena testar e aprender pois está cada vez mais comum obtermos conteúdo através de assistentes virtuais e ter isso a favor do nosso aplicativo pode ser um alto ganho de usabilidade e negócio.

O código completo do projeto abordado neste artigo pode ser encontrado no [Github](https://github.com/anacoimbrag/recipes-assistant).