Como Impulsionar o App Android - Compartilhamento Nativo

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes! Você receberá um email de confirmação. Somente depois de confirma-lo é que eu poderei lhe enviar os conteúdos semanais exclusivos. Os artigos em PDF são entregues somente para os inscritos na lista.

Email inválido.
Blog /Android /Como Impulsionar o App Android - Compartilhamento Nativo

Como Impulsionar o App Android - Compartilhamento Nativo

Vinícius Thiengo
(5640) (8)
Go-ahead
"O método consciente de tentativa e erro é mais bem-sucedido que o planejamento de um gênio isolado."
Peter Skillman
Prototipagem Android
Capa do curso Prototipagem Profissional de Aplicativos
TítuloAndroid: Prototipagem Profissional de Aplicativos
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
Acessar Curso
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Lendo
TítuloManual de DevOps: como obter agilidade, confiabilidade e segurança em organizações tecnológicas
CategoriaEngenharia de Software
Autor(es)Gene Kim, Jez Humble, John Willis, Patrick Debois
EditoraAlta Books
Edição
Ano2018
Páginas464
Conteúdo Exclusivo
Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba gratuitamente conteúdos Android sem precedentes!
Email inválido

Tudo bem?

Neste artigo vamos estudar como compartilhar conteúdos em aplicativos Android utilizando APIs nativas, mais precisamente, utilizando Intent e IntentFilter.

Como projeto de exemplo desenvolveremos um aplicativo de notícias onde será possível compartilhar qualquer notícia, incluindo a imagem remota vinculada a notícia em compartilhamento:

Animação app Android Brasil News

Antes de prosseguir saiba que aqui utilizaremos o termo "API" como sinônimo de "biblioteca" (library).

Se inscreva 📩 na lista de emails do Blog para ter acesso aos conteúdos exclusivos sobre desenvolvimento Android.

A seguir os tópicos abordados:

Compartilhamento via API nativa

Quando falamos de compartilhamento é comum pensar em redes sociais, mas na verdade até mesmo o envio de dados de uma atividade a outra, no mesmo aplicativo, é uma forma de compartilhamento de conteúdo.

Felizmente o Android tem as intenções, ou Intents, que nos permitem a fácil comunicação entre atividades / aplicativos sem, principalmente, a dependência de APIs específicas para permitir, por exemplo, o compartilhamento de algum dado.

Com as Intents é possível compartilhar conteúdos simples, como textos, até conteúdos complexos que envolvem também binários (imagem, vídeo, áudio, arquivos, ...).

É importante ressaltar que o compartilhamento de conteúdo depende também do aplicativo que receberá os dados em compartilhamento, pois de acordo com a configuração do app receptor não é possível que este seja uma opção de share.

Neste artigo nosso foco é no compartilhamento utilizando Intents em um aplicativo emissor, em futuros conteúdos trabalharemos também como fazer com que um aplicativo Android possa ser um receptor de conteúdos compartilhados via Intent.

Conteúdo simples, não binário

Compartilhar conteúdo somente em texto é bem simples. Veja o código a seguir:

...
val intent = Intent()

intent.action = Intent.ACTION_SEND
intent.putExtra( Intent.EXTRA_TEXT, "Texto de exemplo." )
intent.type = "text/plain"
...

 

Nos conteúdos que temos aqui no Blog sobre Intent e IntentFilter, conteúdos indicados como links logo no início deste artigo, nesses há explicações detalhadas da importância de cada propriedade dessas classes.

Mas adianto a ti que não há com que se preocupar se você ainda não conhece a fundo as entidades Intent e IntentFilter, isso, pois as configurações de compartilhamento são simples e com rótulos autocomentados.

O código anterior é válido também para compartilhamento de links, domínios públicos.

As propriedades action e type são importantes para que aplicativos receptores possam ou não aparecer como opções de compartilhamento.

Executando o projeto com o código anterior, temos:

Compartilhamento de conteúdo somente em texto

Verificação de segurança

É prudente colocarmos uma verificação de segurança antes da invocação de startActivity():

...
val intent = Intent()

intent.action = Intent.ACTION_SEND
intent.putExtra( Intent.EXTRA_TEXT, "Texto de exemplo." )
intent.type = "text/plain"

if( intent.resolveActivity( packageManager ) != null ) {
startActivity( intent )
}
...

 

Esta verificação é recomendada principalmente para compartilhamentos onde somente aplicativos específicos são aceitos, apps definidos na propriedade package de Intent. Em seções posteriores abordaremos a propriedade package.

Caixa de diálogo para a escolha de aplicativo

A Chooser Dialog aparecerá somente quando mais de um aplicativo no aparelho do usuário for um possível alvo do conteúdo que está para ser compartilhado:

Android Chooser Dialog padrão

Em casos onde somente um aplicativo é passível para compartilhamento do conteúdo, esse app será aberto diretamente.

É possível customizarmos a Chooser Dialog, colocando um título personalizado. Veja o código a seguir:

...
val intent = Intent()

intent.action = Intent.ACTION_SEND
intent.putExtra( Intent.EXTRA_TEXT, "Texto de exemplo com chooser ativado." )
intent.type = "text/plain"

if( intent.resolveActivity( packageManager ) != null ) {
val intentChooser = Intent.createChooser( intent, "Compartilhar notícia com:" )
startActivity( intentChooser )
}
...

 

O uso de createChooser() é válido para qualquer tipo de conteúdo em Intent de compartilhamento, não somente conteúdo em texto.

Executando o projeto com o código anterior, temos:

Android Chooser Dialog personalizada

Conteúdo HTML

Compartilhar conteúdo HTML é tão simples quanto o compartilhamento de conteúdo em texto. Veja o código a seguir:

...
val intent = Intent()

intent.action = Intent.ACTION_SEND
intent.putExtra( Intent.EXTRA_TEXT, "<b>Texto de exemplo.</b>" )
intent.type = "text/html"

if( intent.resolveActivity( packageManager ) != null ) {
startActivity( intent )
}
...

 

Executando o projeto de teste com o código anterior, temos:

Compartilhamento de conteúdo em HTML

Email

Uma das técnicas que mais utilizo em aplicativos simples que também permitem o contato com clientes é o uso de intenções para o fácil envio de email à empresa do aplicativo.

A configuração segura para envio de email é a seguinte:

...
val intent = Intent()

/*
* Segundo alguns testes a ação Intent.ACTION_SENDTO é
* parte da configuração para que ao menos todos os
* aplicativos de email aparecerão como opções de
* compartilhamento, mas para conteúdos de
* compartilhamento que também têm binários (imagem,
* vídeo, ...) é recomendado o uso de Intent.ACTION_SEND.
* */
intent.action = Intent.ACTION_SENDTO

/*
* Apesar do uso da ação Intent.ACTION_SENDTO, alguns
* aplicativos de email somente aparecerão como opções
* de compartilhamento se a Uri "mailto:" for definida
* na propriedade data.
* */
intent.data = Uri.parse( "mailto:" )

/*
* Forneça os destinatários (incluindo os destinatários
* em cópia ou em cópia oculta) dentro de um array de
* Strings.
* */
intent.putExtra(
Intent.EXTRA_EMAIL,
arrayOf( "thiengocalopsita@gmail.com" )
)
intent.putExtra(
Intent.EXTRA_CC,
arrayOf( "admin@thiengo.com.br" )
)
intent.putExtra(
Intent.EXTRA_BCC,
arrayOf( "account@thiengo.com.br" )
)

/*
* Definindo o assunto do email.
* */
intent.putExtra(
Intent.EXTRA_SUBJECT,
"Apenas um teste - Assunto"
)

/*
* Definindo o conteúdo em texto do corpo do email.
* */
intent.putExtra(
Intent.EXTRA_TEXT,
"Apenas um teste de envio de email por Intent - Conteúdo"
)

if( intent.resolveActivity( packageManager ) != null ) {
val intentChooser = Intent.createChooser( intent, "Enviar email com:" )
startActivity( intentChooser )
}
...

 

Não deixe de ler os comentários do código anterior para entender o porquê de cada configuração para aplicativos de email.

Executando o projeto com o código acima, temos:

Compartilhamento em email

Como no emulador de testes somente havia um aplicativo que respondia às configurações de email que colocamos em Intent, mais precisamente o aplicativo Gmail, então o acionamento do botão "COMPARTILHAR POR EMAIL" fez com que este aplicativo abrisse direto, sem uma Chooser Dialog.

No projeto de exemplo deste artigo teremos um algoritmo para o compartilhamento de conteúdo binário e em texto via email.

Conteúdo binário (imagem, vídeo, áudio, ...) interno ao app

Agora se inicia as seções que provavelmente você estava aguardando, sobre compartilhamento de conteúdos binários. Aqui utilizaremos como exemplo binários de imagens.

A seguir o código de compartilhamento de um binário que está dentro do aplicativo, mais precisamente dentro do folder drawable:

...
/*
* Apesar de no exemplo estarmos utilizando uma imagem JPEG, qualquer
* conteúdo binário interno ao aplicativo é passível de ser
* compartilhado como no código a seguir, somente certifique-se de
* definir o type correto.
* */
val internalReference = "android.resource://thiengo.com.br.androidnativeshare/drawable/bmw_x2"
val uri = Uri.parse( internalReference )

val intent = Intent()

intent.action = Intent.ACTION_SEND

/*
* Quando o mime type exato (exemplo: image/jpeg) não é utilizado
* e sim o genérico, image/{*}, o aplicativo receptor perde a
* precisão sobre o conteúdo sendo compartilhado e assim não
* mostra, por exemplo, a miniatura do arquivo binário. É possível
* que aplicativos nem mesmo apareçam como opção de compartilhamento
* se um tipo específico de mime não for definido.
* */
intent.type = "image/jpeg"

/*
* Com a flag abaixo definida, o destinatário da Intent receberá
* permissão para realizar operações de leitura na URI dos dados em
* compartilhamento.
* */
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION

/*
* Quando compartilhando conteúdo binário (imagem, vídeo, áudio, ...)
* é aguardada a URI do conteúdo, independente se o conteúdo está ou
* não dentro do aplicativo.
* */
intent.putExtra( Intent.EXTRA_STREAM, uri )

if( intent.resolveActivity( packageManager ) != null ) {
val intentChooser = Intent.createChooser( intent, "Compartilhar imagem com:" )
startActivity(intentChooser)
}
...

 

Executando o projeto com o código anterior, temos:

Compartilhando binário e solicitando permissão

Depois, salvando o binário:

Salvando imagem no Google Drive app

É importante lembrar da autoridade que aplicativos receptores têm em relação ao conteúdo compartilhado com eles. Mesmo quando esses aparecem como apps passíveis de compartilhamento é possível de acontecer a não aceitação de alguns conteúdos em Intent.

Veja a seguir o resultado da tentativa de compartilhamento, utilizando o código anterior, no app do Gmail:

Compartilhamento falho no app do Gmail

Por algum motivo o aplicativo do Gmail não consegue anexar binários que estão dentro do aplicativo emissor, mesmo se o app do Gmail estiver com a permissão de acesso a conteúdo em armazenamento externo.

Temos de entender que essa é uma limitação do aplicativo do Gmail para binários internos ao aplicativo. Certamente deve ter algum algoritmo que permite esse contorno, ou seja, a compartilhamento de binário interno com o app do Gmail, mas deveria ser algo trivial como foi no caso do Google Drive app.

Note que não há a necessidade de nosso aplicativo ter acesso a permissão de armazenamento externo para compartilhar um binário interno a ele, mas o aplicativo receptor tem de ter essa permissão e é ele que tem de solicitar essa permissão quando no momento do compartilhamento, algo que não ocorre com o aplicativo do Gmail, mas é realizado com maestria pelo app do Google Drive.

Conteúdo binário do armazenamento externo

Para o compartilhamento de conteúdo binário externo ao aplicativo, são necessárias ainda mais configurações extras.

A primeira é a criação de um arquivo de configuração de FileProvider, arquivo que nos permitirá definir a área do armazenamento externo ao aplicativo que poderemos acessar para obter conteúdos binários.

É recomendado que este arquivo de configuração esteja em /res/xml e tenha um nome autocomentado sobre a função dele, aqui o nome será file_path.xml:

<?xml version="1.0" encoding="utf-8"?>
<paths>
<!--
Com a configuração a seguir estamos indicando que o objeto
FileProvider de nosso aplicativo pode acessar qualquer
arquivo presente no diretório Download/ do armazenamento
externo, ou seja, do SDCard.

O path root indicado pelo uso de <external-path> é o mesmo
retornado pela invocação de
Environment.getExternalStorageDirectory() em código dinâmico.

O atributo "name" é utilizado para ocultar o nome real do
subdiretório definido no atributo "path", reforçando a
segurança de compartilhamento de conteúdo.

O atributo "path" contém o subdiretório que é passível de
ter seus arquivos acessados para compartilhamento. Para
permitir acesso ao qualquer arquivo do armazenamento,
coloque apenas um path=".".
-->
<external-path
name="all_paths"
path="Download/" />
</paths>

 

É possível ter inúmeros "caminhos" (diretórios) como elementos de <paths>. Não é possível colocar como valor do atributo path a definição de um arquivo em específico, somente diretório.

Para conhecer as outras possíveis tags filhas de <paths>, acesse a documentação oficial em: Specifying Available Files.

Antes de colocarmos o código de configuração do FileProvider no AndroidManifest.xml é preciso saber o que é um FileProvider. Segundo a documentação:

FileProvider é uma subclasse especial de ContentProvider que facilita o compartilhamento seguro de arquivos associados a um aplicativo, mesmo arquivos externos a ele, criando uma Uri content:// para um arquivo ao invés vez de uma Uri file:/// que tem mais limitações em compartilhamento.

Resumidamente: um objeto FileProvider vai nos permitir o compartilhamento seguro de qualquer arquivo presente nos paths definidos em <paths> sem que o app receptor do compartilhamento tenha as permissões de leitura e escrita no armazenamento externo, isso, pois essas permissões são concedidas temporariamente devido à Uri gerada ser uma Uri ContentProvider.

Certo, já temos o arquivo de configuração de área de acesso do FileProvider, mas e a definição do FileProvider?

Está vem no arquivo AndroidManifest.xml como um filho direto da tag <application>:

<application ...>

<!--
Abaixo a definição da criação de um objeto FileProvider,
definição estática.

android:name contém o tipo de objeto que será criado.

android:authorities defini a autoridade de URI baseada em
um domínio que você controla; por exemplo, se você controlar
o domínio público meudominio.com.br, deverá usar a
autoridade br.com.meudominio.fileprovider. No exemplo a
seguir utilizamos o nome do package do projeto, mas isso não
é obrigatório.

android:exported é para acesso ao objeto FileProvider, acesso
somente privado ao aplicativo de criação dele, logo, coloque
aqui o valor false.

android:grantUriPermissions deve conter o valor true para que
os aplicativos receptores do arquivo compartilhado possam ter
as permissões necessárias de maneira temporária casos esses
já não tenham elas definida e liberadas pelo usuário.
-->
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="thiengo.com.br.androidnativeshare.fileprovider"
android:exported="false"
android:grantUriPermissions="true">

<!--
Definição do arquivo de configuração das áreas de acesso
do FileProvider. Neste caso o valor de android:name tem
de ser android.support.FILE_PROVIDER_PATHS.

android:resource recebe o nome do arquivo com as
configurações de áreas de acesso. Não coloque a
extensão .xml
-->
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_path" />
</provider>
...
</application>

 

Antes de partirmos para o código com a Intent de compartilhamento de binário externo ao app, ainda é preciso colocarmos o algoritmo de solicitação de permissão em tempo de execução.

Primeiro as permissões necessárias, em AndroidManifest.xml:

<manifest ...>

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
...
</manifest>

 

Agora a vinculação da biblioteca EasyPermissions ao projeto de teste, está será utilizada na administração do algoritmo de solicitação de permissão em tempo de execução. A seguir a adição da API no Gradle Nível de Aplicativo, build.gradle (Module: app):

...
dependencies {
...
implementation 'pub.devrel:easypermissions:1.2.0'
}

 

Então o código de solicitação de permissão na classe que conterá o algoritmo de compartilhamento de binário via Intent:

class MainActivity :
AppCompatActivity(),
EasyPermissions.PermissionCallbacks {
...

companion object {
const val PERMISSION_STORAGE = 2256
}

/*
* O primeiro passo é verificar se as permissões de
* STORAGE foram concedidas, caso ainda não elas devem
* ser solicitadas ao usuário. O algoritmo do método
* abaixo faz todo este trabalho de verificação e
* solicitação.
* */
fun askPermissionToShareBinaryContent( view: View ){

EasyPermissions.requestPermissions(
PermissionRequest
.Builder(
this,
PERMISSION_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
.build()
)
}

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray ) {

super.onRequestPermissionsResult(
requestCode,
permissions,
grantResults )

EasyPermissions.onRequestPermissionsResult(
requestCode,
permissions,
grantResults,
this )
}

/*
* Em caso de permissões concedidas, este é o método que
* deve invocar o algoritmo de compartilhamento de dado
* binário.
* */
override fun onPermissionsGranted(requestCode: Int, perms: MutableList<String>) {
/* TODO */
}

override fun onPermissionsDenied(requestCode: Int, perms: MutableList<String>) {}
}

 

O método askPermissionToShareBinaryContent() é o que primeiro deve ser invocado antes do acesso ao algoritmo de compartilhamento.

Para saber mais sobre as vantagens de uso da API EasyPermissions e ainda mais possibilidades com ela, não deixe de acessar meu novo livro Desenvolvedor Kotlin Android - Bibliotecas para o dia a dia onde o primeiro capítulo tem 55 páginas dedicadas somente a está API e a um exemplo de aplicativo utilizando ela.

Com isso somente temos de colocar em onPermissionsGranted() o algoritmo de compartilhamento de binário via Intent:

...
override fun onPermissionsGranted( requestCode: Int, perms: MutableList<String> ) {
shareLocalSDCardBinaryContent()
}
...

fun shareLocalSDCardBinaryContent(){
/*
* É necessário o caminho completo de acesso ao arquivo binário
* no SDCard. Neste exemplo utilizaremos um binário de imagem.
* */
val fileName = "bmw-m4.jpg"
val dir = "Download"

/*
* A invocação Environment.getExternalStorageDirectory()
* representa o mesmo path que o uso de <external-path>
* no arquivo de paths do FileProvider.
* */
val path = Environment.getExternalStorageDirectory()

val file = File( "$path/$dir/$fileName" )

/*
* Como o binário em compartilhamento não está dentro do
* aplicativo emissor, a melhor estratégia para permitir o
* compartilhamento do binário é colocando-o no ContentProvider.
* O segundo argumento de getUriForFile(), que é o package de
* autoridade sobre o binário, têm de ser compatível com o
* definido no AndroidManifest.xml.
* */
val uri = FileProvider.getUriForFile(
this,
"thiengo.com.br.androidnativeshare.fileprovider",
file
)

val intent = Intent()

intent.action = Intent.ACTION_SEND
intent.type = "image/jpeg"
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION

intent.putExtra( Intent.EXTRA_STREAM, uri )

if( intent.resolveActivity( packageManager ) != null ) {
val intentChooser = Intent.createChooser( intent, "Compartilhar imagem com:" )
startActivity(intentChooser)
}
}
...

 

Executando o projeto com o código anterior, temos:

Compartilhamento de imagem local no SDCard Android

Seção grande, não? Mas é assim que deve ser feito quando compartilhando conteúdo binário presente no aparelho, mas não no aplicativo.

Apesar na expressiva quantidade de código, a maior parte dele é boilerplate, exigindo pouca lógica de negócio.

Conteúdo binário remoto

Também é possível compartilhar conteúdos carregados de servidores remotos. Assuma que o código a seguir é responsável por carregar uma imagem no ImageView do layout da atividade principal do app de testes:

class MainActivity : ... {

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

Picasso
.get()
.load( "https://www.challenges.fr/assets/img/2018/05/10/cover-r4x3w1000-5af43ab091a51-p90304062-highres-effortless-everywher.jpg" )
.into( iv_image )
}
...
}

 

A API Picasso é uma das melhores em termos de eficiência e eficácia para carregamento de imagens em aplicativos Android. A adição dela ao projeto é por meio do Gradle Nível de Aplicativo, build.gradle (Module: app):

dependencies {
...
implementation 'com.squareup.picasso:picasso:2.71828'
}

 

Em meu novo livro Desenvolvedor Kotlin Android - Bibliotecas para o dia a dia o segundo capítulo é inteiramente dedicado a essa API, 87 páginas, mostrando ainda mais funcionalidades possíveis com a Picasso Android.

Depois que a imagem já foi carregada na View alvo é possível acessa-la como um Bitmap, mais precisamente como um BitmapDrawable que é um subtipo de Bitmap:

...
val drawable = iv_image.getDrawable() as BitmapDrawable
...

 

Com um Bitmap em "mãos" o que precisamos é de um algoritmo que consiga retornar uma Uri partindo do Bitmap, algoritmo como o a seguir:

...
fun getImageUri( bitmap: Bitmap ): Uri {

val bytes = ByteArrayOutputStream()

/*
* Escreva uma versão compactada de "bitmap" no fluxo
* de saída especificado, "bytes".
* */
bitmap.compress( Bitmap.CompressFormat.JPEG, 100, bytes )

/*
* Insere uma imagem no MediaStore.Images.Media e
* também cria uma miniatura para ela.
* */
val path = MediaStore.Images.Media.insertImage(
contentResolver,
bitmap,
"",
null
)

return Uri.parse( path )
}
...

 

Apesar de contentProvider também está sendo utilizado, para compartilhamento de conteúdo remoto não é necessária a configuração de um FileProvider, como fizemos na seção anterior. Mas aqui também são exigidas as permissões de acesso ao armazenamento externo.

Vou abreviar o algoritmo e assim não mostrarei o código de solicitação de permissão em tempo de execução, este você pode obter na seção anterior com a API EasyPermissions.

Assuma que as permissões de armazenamento já foram concedidas. Assim temos o código final para compartilhamento de um binário de uma fonte remota ao aparelho:

...
fun shareRemoteBinaryContent(){

/*
* Antes de chegar a este ponto é necessário que seu aplicativo
* já tenha a permissão de armazenamento vinculada a ele e que a imagem já
* tenha sido carregada no ImageView.
* */

/*
* Obtendo o Bitmap do ImageView. Assim que a
* imagem é carregada no ImageView é possível,
* posteriormente, obtê-la como BitmapDrawable,
* um subtipo de Bitmap.
* */
val drawable = iv_image.getDrawable() as BitmapDrawable
val bitmap = drawable.bitmap

val bitmapUri = getImageUri( bitmap )

val intent = Intent()

intent.action = Intent.ACTION_SEND
intent.type = "image/jpeg"
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
intent.putExtra( Intent.EXTRA_STREAM, bitmapUri )

if( intent.resolveActivity( packageManager ) != null ) {
val intentChooser = Intent.createChooser(intent, "Compartilhar imagem com:")
startActivity( intentChooser )
}
}
...

 

Executando o projeto de testes com o algoritmo anterior, temos:

Compartilhamento de imagem remota via Intent

O aplicativo que vai receber o binário em compartilhamento não precisa de permissão de armazenamento, somente o app emissor da Intent.

Curiosidade: um nome aleatório é utilizado como rótulo do binário em compartilhamento.

Vários conteúdos binários

As regras de compartilhamento de vários conteúdos binários são as mesmas já discutidas em seções anteriores, ou seja, o algoritmo utilizado para cada conteúdo em compartilhamento depende da origem dele.

No código a seguir, para manter a simplicidade, vamos ao compartilhamento de dois conteúdos binários internos ao aplicativo, algo que dispensa a necessidade de qualquer código de solicitação de permissão em tempo de execução:

...
val uriInternalImages = arrayListOf(
Uri.parse("android.resource://thiengo.com.br.androidnativeshare/drawable/bmw_x2"),
Uri.parse("android.resource://thiengo.com.br.androidnativeshare/drawable/bmw_m4")
)

val intent = Intent()

intent.action = Intent.ACTION_SEND_MULTIPLE
intent.type = "image/jpeg"
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
intent.putExtra( Intent.EXTRA_STREAM, uriInternalImages )

if( intent.resolveActivity( packageManager ) != null ) {
val intentChooser = Intent.createChooser(intent, "Compartilhar imagens com:")
startActivity( intentChooser )
}
...

 

Note o tipo da ação, Intent.ACTION_SEND_MULTIPLE, e como as Uris têm de ser fornecidas em um ArrayList.

Executando o projeto de testes com o algoritmo anterior, temos:

Compartilhando duas imagens com o Google Drive app

Diferentes tipos de dados

É possível compartilhar diferentes tipos de dados em um só Intent. O aplicativo receptor é que deverá tratar corretamente cada tipo em compartilhamento.

Caso haja também binários no compartilhamento, todas as regras de negócio discutidas em seções anteriores deverão ser levadas em conta de acordo com a origem de cada binário.

No código abaixo estaremos compartilhando junto com um texto um binário remoto, logo, assuma que as permissões de armazenamento foram concedidas e que o método getImageUri() é o mesmo utilizado na seção Conteúdo binário remoto.

Segue:

...
val drawable = iv_image.getDrawable() as BitmapDrawable
val bitmap = drawable.bitmap
val bitmapUri = getImageUri( bitmap )

val text = "Esse é um SUV Rolls-Royce"

val intent = Intent()

intent.action = Intent.ACTION_SEND
intent.type = "*/*"
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION

intent.putExtra( Intent.EXTRA_STREAM, bitmapUri )
intent.putExtra( Intent.EXTRA_TEXT, text )

if( intent.resolveActivity( packageManager ) != null ) {
val intentChooser = Intent.createChooser( intent, "Compartilhar conteúdo com:" )
startActivity( intentChooser )
}
...

 

Executando o projeto de teste com o algoritmo anterior, temos:

Compartilhando múltiplos conteúdos com o app Gmail

Conteúdos para aplicativos específicos

Caso seu aplicativo possa compartilhar determinados conteúdos somente com algum outro app, é possível restringir esse "algum outro app" colocando o nome de pacote dele no Intent de compartilhamento, isso utilizando o método setPackage().

No código a seguir somente o Gmail pode receber o conteúdo compartilhado:

...
val intent = Intent()

intent.action = Intent.ACTION_SEND
intent.type = "text/plain"

/*
* Somente o Gmail pode receber este conteúdo de
* compartilhamento.
* */
intent.setPackage( "com.google.android.gm" )

intent.putExtra(
Intent.EXTRA_TEXT,
"Compartilhando conteúdo direto no Gmail."
)

if( intent.resolveActivity( packageManager ) != null ) {
startActivity( intent )
}
else{
Toast
.makeText(
this@MainActivity,
"Instale o aplicativo Gmail",
Toast.LENGTH_SHORT
)
.show()
}
...

 

O bloco condicional para verificação de presença de aplicativo compatível com o conteúdo em compartilhamento é muito importante quando um package é fornecido, pois é possível que o aplicativo não esteja instalado no aparelho.

Executando o projeto com o algoritmo anterior, temos:

Compartilhamento somente com o Gmail app

Caso você precise definir mais de um aplicativo para possível compartilhamento, faça como no código a seguir:

...
val intentGmail = Intent()
intentGmail.action = Intent.ACTION_SEND
intentGmail.type = "text/plain"
intentGmail.setPackage( "com.google.android.gm" )
intentGmail.putExtra( Intent.EXTRA_TEXT, "Gmail app teste." )

val intentDrive = Intent()
intentDrive.action = Intent.ACTION_SEND
intentDrive.type = "text/plain"
intentDrive.setPackage( "com.google.android.apps.docs" )
intentDrive.putExtra( Intent.EXTRA_TEXT, "Drive app teste." )

val intentMessaging = Intent()
intentMessaging.action = Intent.ACTION_SEND
intentMessaging.type = "text/plain"
intentMessaging.setPackage( "com.google.android.apps.messaging" )
intentMessaging.putExtra( Intent.EXTRA_TEXT, "Mensagens app teste." )

/*
* Coloque no array de Intents todos os Intents exceto o
* que será colocado em Intent.createChooser().
* */
val intents = arrayOf(
intentDrive,
intentMessaging
)

if( intent.resolveActivity( packageManager ) != null ) {

/*
* Um Intent dos que têm o setPackage() definido
* deve entrar no createChooser(). Todos os outros
* Intents devem estar no array que entrará em
* putExtra() junto a chave Intent.EXTRA_INITIAL_INTENTS.
* */
val intentChooser = Intent.createChooser(
intentGmail, "Compartilhar conteúdo com:"
)

intentChooser.putExtra(
Intent.EXTRA_INITIAL_INTENTS,
intents
)

startActivity( intentChooser )
}
...

 

Executando o projeto com o código anterior, temos:

Compartilhamento com apps especificados

Por que o conteúdo limitado sobre aplicativos sociais?

Na verdade tudo que foi apresentado até aqui é passível de ser utilizado em qualquer aplicativo social, porém alguns apps de redes sociais apresentam limitações.

O aplicativo do Facebook, por exemplo, tem problemas com conteúdos em compartilhamento que contém binários e não binários. Como solução o Facebook tem uma API nativa somente para compartilhamento via aplicativos Android.

Na seção anterior, onde utilizamos como exemplo pacotes de aplicativos populares, tem o modelo de algoritmo que permite também o compartilhamento somente com apps sociais, caso seja necessária essa restrição.

De qualquer forma, havendo a possibilidade de compartilhamento de binários, busque primeiro pela API nativa do app alvo como receptor do compartilhamento.

Pontos negativos

  • O compartilhamento de conteúdo binário no armazenamento local não é nada trivial, exigindo o entendimento de entidades, FilePovider, que provavelmente não voltaremos a utilizar;
  • Mesmo que a configuração de compartilhamento via Intent esteja bem construída, ainda dependemos da qualidade do algoritmo do aplicativo receptor.

Pontos positivos

  • Comparando o código de compartilhamento via APIs nativas com o código de APIs de terceiros, o de APIs nativas é ainda a melhor opção devido à simplicidade quando não há, por exemplo, um binário, no armazenamento local, a ser compartilhado;
  • A fácil possiblidade de definirmos os aplicativos que podem receber o conteúdo em compartilhamento faz com que muito código extra de API específica seja evitado no projeto.

Considerações finais

Provavelmente você é um daqueles desenvolvedores Android que quer ver o seu aplicativo sendo utilizado pela massa de usuários.

Um dos algoritmos que podem ajudar isso acontecer, a adoção da massa, é o algoritmo de compartilhamento.

Adicionar o compartilhamento via APIs nativas é, até o momento, a melhor opção, além de não exigir a adição de referências extras que inflariam ainda mais o tamanho final do aplicativo.

Projeto Android

Para projeto de exemplo vamos construir um aplicativo de notícias que permitirá o compartilhamento da imagem, texto e conteúdo de qualquer notícia.

O desenvolvimento do projeto será dividido em duas partes:

  • Primeiro vamos a construção do aplicativo sem o código de compartilhamento;
  • Na segunda parte vamos adicionar o algoritmo de compartilhamento, com foco em aplicativos de email.

Para ter acesso ao aplicativo finalizado, entre no GitHub dele em: https://github.com/viniciusthiengo/brasil-news-kotlin-android.

Não deixe de acompanhar a construção do projeto em artigo, pois vamos passo a passo a explicação de algumas partes importantes no desenvolvimento de aplicativos Android, além do algoritmo de compartilhamento de conteúdo com apps de email, compartilhamento contendo também dado binário.

Protótipo estático

A seguir o protótipo estático do projeto:

Tela de abertura

Tela de abertura

Tela de listagem de notícias

Tela de listagem de notícias

Menu gaveta aberto

Menu gaveta aberto

Tela de detalhes de notícia

Tela de detalhes de notícia

Dialog de compartilhamento aberto

Dialog de compartilhamento aberto

 


Iniciando o projeto

Em seu Android Studio inicie um novo projeto Kotlin:

  • Nome da aplicação: Brasil News;
  • API mínima: 16 (Android Jelly Bean);
  • Atividade inicial: Navigation Drawer Activity;
  • Nome da atividade inicial: NewsActivity;
  • Para todos os outros campos, mantenha os valores já definidos por padrão.

Ao final da primeira parte do projeto teremos a seguinte arquitetura:

Arquitetura Android Studio do app Brasil News

Configurações Gradle

Abaixo as configurações do arquivo Gradle Nível de Projeto, ou build.gradle (Project: BrasilNews):

buildscript {
ext.kotlin_version = '1.3.0'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
google()
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

 

Então a configuração do Gradle Nível de Aplicativo, ou build.gradle (Module: app):

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 28
defaultConfig {
applicationId "thiengo.com.br.brasilnews"
minSdkVersion 16
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support:design:28.0.0'

implementation 'com.squareup.picasso:picasso:2.71828'
}

Configurações AndroidManifest

Abaixo as configurações iniciais do AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="thiengo.com.br.brasilnews">

<uses-permission android:name="android.permission.INTERNET"/>

<application
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">

<activity
android:name=".NewsActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">

<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

<activity
android:parentActivityName=".NewsActivity"
android:name=".NewsDetailsActivity"
android:label="@string/title_activity_news_details"
android:theme="@style/AppTheme.NoActionBar">

<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="thiengo.com.br.brasilnews.NewsActivity" />
</activity>
</application>
</manifest>

Configurações de estilo

Para os arquivos de estilo presentes em /res/values, vamos iniciar com o XML de definição de cores utilizadas no aplicativo, /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorBackground">#EEEEEE</color>
<color name="colorNavigation">#F5F5F6</color>

<color name="colorPrimary">#212121</color>
<color name="colorPrimaryDark">#000000</color>
<color name="colorAccent">#FFA726</color>
<color name="colorText">#222222</color>

<color name="colorItemNormal">#777777</color>

<color name="colorLine">#CCCCCC</color>

<color name="colorFavButton">#AAAAAA</color>
<color name="colorShareButton">#00A6FF</color>
</resources>

 

Agora o simples arquivo de definição de dimensões, /res/values/dimens.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>

 

Abaixo o arquivo de Strings, /res/values/strings:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Brasil News</string>

<string name="navigation_drawer_open">Abrir menu gaveta</string>
<string name="navigation_drawer_close">Fechar menu gaveta</string>

<string name="action_search">Buscar</string>

<string name="image_of_label">Imagem de</string>

<string name="item_label_news">Todas as notícias do dia</string>
<string name="item_label_politics">Política</string>
<string name="item_label_business">Negócios</string>
<string name="item_label_sport">Esporte</string>
<string name="item_label_science_and_tec">Ciências e tecnologia</string>
<string name="item_label_health">Saúde</string>
<string name="item_label_entertainment">Entretenimento</string>
<string name="title_activity_news_details">NewsDetailsActivity</string>
<string name="bt_label_fav">Botão de favorito</string>
<string name="bt_label_share">Botão de compartilhar</string>

<string name="permission_inform">
Forneça a permissão de acesso ao SDCard para
que seja possível compartilhar também a imagem
do artigo.
</string>

<string name="chooser_title">
Compartilhar artigo em:
</string>

<string name="share_without_image">
Compartilhamento sem imagem.
</string>

<string name="initial_share_body">
(Brasil News App - Google Play Store)
\n\n
Artigo:
</string>
</resources>

 

Então os arquivos de definição de tema. Primeiro o arquivo genérico, que serve para todas as versões Android atendidas pelo aplicativo (a partir da versão 16, Jelly Bean).

Segue XML /res/values/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>

<!--
Estilo padrão, aplicado em todo o projeto.
-->
<style
name="AppTheme"
parent="Theme.AppCompat.Light.DarkActionBar">

<item name="android:windowBackground">@drawable/background</item>

<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>

<!--
Para que a barra de topo padrão não seja utilizada e
assim somente o AppBarLayout junto ao Toolbar possam ser
usados.
-->
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>

<!--
Para o correto enquadramento do AppBarLayout.
-->
<style
name="AppTheme.AppBarOverlay"
parent="ThemeOverlay.AppCompat.Dark.ActionBar"/>

<!--
Utilizado para a correta apresentação de menus de pop-up
em barra de topo.
-->
<style
name="AppTheme.PopupOverlay"
parent="ThemeOverlay.AppCompat.Light"/>
</resources>

 

Assim o arquivo de tema com algumas particularidades para versões do Android a partir da API 21, Lollipop.

Segue /res/values-v21/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>

<!--
Para que a barra de topo padrão não seja utilizada e
assim somente o AppBarLayout junto ao Toolbar possam ser
usados. Somando a isso a aplicação de transparência na
statusBar.
-->
<style name="AppTheme.NoActionBar">

<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>

Pacote de domínio

Teremos somente uma classe de domínio, representante das notícias em aplicativo.

Primeiro adicione ao projeto um novo pacote, /domain. Então adicione a classe News como a seguir:

class News(
val title: String,
val imageUrl: String,
val description: String
) : Parcelable {

constructor( source: Parcel ) : this(
source.readString(),
source.readString(),
source.readString()
)

override fun describeContents() = 0

override fun writeToParcel( dest: Parcel, flags: Int ) = with( dest ) {
writeString( title )
writeString( imageUrl )
writeString( description )
}

companion object {
@JvmStatic
val KEY = "news_item"

@JvmField
val CREATOR: Parcelable.Creator<News> = object : Parcelable.Creator<News> {
override fun createFromParcel( source: Parcel ): News = News( source )
override fun newArray( size: Int ): Array<News?> = arrayOfNulls( size )
}
}
}

 

A implementação do Parcelable se fez necessária devido ao transporte de objetos News da atividade principal para a atividade de detalhes, isso por meio de Intent.

Classe de dados

Crie um novo pacote na raiz do projeto, pacote /data. Nele teremos uma classe de dados simulados, Database, como no código a seguir:

class Database {

companion object {
fun getNews() =
listOf(
News(
"Portal Uai e Chromos divulgam gabarito extraoficial do segundo dia de Enem",
"https://i.em.com.br/ckKq8Gn6PMz2_keqRrI_eYIGcjg=/675x0/smart/imgsapp.em.com.br/app/noticia_127983242361/2018/11/11/1004883/20181111210636481710a.jpg",
"Estudantes de todo o Brasil fizeram neste domingo a segunda etapa de " +
"provas do Exame Nacional do Ensino Médio (Enem). As questões foram relacionadas " +
"às Ciências da Natureza e Matemática. Com o objetivo de participar da jornada " +
"dos candidatos do Enem 2018, o Chromos Colégio e Preparatório, em parceria com " +
"o Portal Uai, promove a correção e divulgação antecipada do gabarito extraoficial " +
"pelo sétimo ano consecutivo.\n\nNo último domingo, 4, o exame teve questões de " +
"linguagem, ciências humanas e redação, que abordaram temas como direitos humanos, " +
"racismo, ditadura militar e violência contra a mulher. O tema da redação foi " +
"“Manipulação do comportamento do usuário pelo controle de dados na internet”.\n\nNos " +
"dois domingos de prova do Enem, a equipe de professores do Chromos, além de fechar " +
"e lançar as respostas, gravou vídeos, em tempo real, resolvendo e comentando todas " +
"as questões."
),
News(
"Reforma trabalhista completa um ano neste domingo",
"https://odia.ig.com.br/_midias/jpg/2018/11/11/700x470/1_carteira_de_trabalho-8614697.jpg",
"Brasília - A reforma trabalhista completa um ano neste domingo. A legislação " +
"alterou mais de 100 pontos da Consolidação das Leis do Trabalho (CLT) e institui " +
"novas forma de contratação, como a modalidade de trabalho intermitente e a " +
"formalização do teletrabalho.\n\nOutras mudanças foram a demissão por meio de " +
"acordo entre empregado e patrão, formalização do teletrabalho, divisão das " +
"férias em três períodos e o fim da obrigatoriedade da contribuição sindical.\n\nNa " +
"avaliação do Ministério do Trabalho, trabalhadores e empregadores ainda estão " +
"se adaptando às novas normas. “Acreditamos que a implantação da Lei 13.467 ainda " +
"está em curso, e, talvez, demande mais algum tempo para se consolidar em nosso " +
"mercado. No entanto, vemos que a cultura das relações de trabalho está mudando e " +
"isso é bom. É um processo gradual”, disse o secretário-executivo substituto da " +
"pasta, Admilson Moreira dos Santos, em nota publicada no site do ministério.\n\nVieira " +
"lembra que algumas das novidades trazidas pela reforma, como o trabalho intermitente, " +
"em que o empregador chama o trabalhador de acordo quando necessário, também acabaram " +
"não gerando um volume de contratações como imaginado."
),
News(
"Bolsonaro vai a praia, saca dinheiro e faz churrasco em casa, no Rio",
"https://s2.glbimg.com/IXPsK2h2czOKWHe7ivVIkl6ufpk=/0x0:1280x960/2000x0/smart/filters:strip_icc()/i.s3.glbimg.com/v1/AUTH_59edd422c0c84a879bd37670ae4f538a/internal_photos/bs/2018/P/C/V4sZGETAWL6YJMAf4GPA/4fa1c334-37a8-4bc2-9e41-3b0182ed9e42.jpg",
"O presidente eleito Jair Bolsonaro saiu de casa pela manhã neste domingo (11) " +
"e, conforme sua assessoria de imprensa, foi ao banco sacar dinheiro a fim de " +
"realizar um churrasco para sua equipe de segurança.\n\nEm seguida, Bolsonaro " +
"passou pela orla da praia, cumprimentou apoiadores e posou para fotos. A " +
"assessoria do presidente eleito divulgou fotos e vídeos do passeio neste domingo, " +
"bem como do churrasco na casa dele, na Barra da Tijuca, Zona Oeste do Rio de " +
"Janeiro.\n\nEm um dos vídeos, Bolsonaro apareceu fazendo o fogo para o churrasco. " +
"Ele estava vestido com uma camisa do América, clube de futebol do Rio – o " +
"presidente é torcedor do Palmeiras e costuma afirmar que no Rio torce para o " +
"Botafogo.\n\nBolsonaro tem previsão de retornar a Brasília, onde funciona o " +
"gabinete de transição do governo, na próxima terça-feira (13) pela manhã.\n\nEle " +
"teria encontros com os presidentes da Câmara e do Senado, Rodrigo Maia (DEM-RJ) " +
"e Eunício Oliveira (MDB-CE). As reuniões foram canceladas, de acordo com a " +
"assessoria do grupo de transição do futuro governo. O motivo não foi informado."
),
News(
"Após mais de 90 horas interditada, rodovia dos Tamoios é liberada",
"https://s2.glbimg.com/8dEC3TcRubB1lYPwnhqQEBqJi70=/0x0:1280x960/2000x0/smart/filters:strip_icc()/i.s3.glbimg.com/v1/AUTH_59edd422c0c84a879bd37670ae4f538a/internal_photos/bs/2018/7/V/RVGXRBTFuOaktNcaBRNw/whatsapp-image-2018-11-11-at-4.54.39-pm.jpeg",
"Após mais de 90 horas de interdição, o tráfego na rodovia dos Tamoios " +
"(SP-99) foi liberado às 17h deste domingo (11). A rodovia foi interditada na " +
"quarta-feira (7) devido às fortes chuvas que registraram pelo menos 23 " +
"ocorrências entre deslizamentos de terra e quedas de árvores.\n\nA rodovia " +
"foi bloqueada no fim da noite de quarta-feira (7) depois do registro de queda " +
"de barreira nos trechos de serra e planalto. Foram registrados pontos de " +
"obstrução em vários pontos entre os quilômetros 69, 71 e 78 entre " +
"Caraguatatuba e Paraibuna.\n\nDesde o bloqueio, equipes da rodovia atuavam " +
"na limpeza e recuperação dos estragos causados pelas fortes chuvas. De acordo " +
"com a Tamoios, foram necessários cerca de cem funcionários e trinta " +
"máquinas para o trabalho de retirada de terra de encostas, árvores e " +
"postes.\n\nNeste sábado (10) a via já havia sido desobstruída, mas a " +
"concessionária aguardava uma avaliação de novos riscos de deslizamentos. O " +
"protocolo de segurança da via exige que a Tamoios seja interditada caso os " +
"pluviômetros registrem cem milímetros em 72 horas. Não chove na região há " +
"48 horas."
),
News(
"Silvio Santos diz torcer por “8 anos de Bolsonaro e mais 8 de Sergio Moro” na presidência",
"https://s.gospelprime.com.br/wp-content/uploads/sites/2/2018/11/silvio-santos-conversa-com-jair-bolsonaro-no-teleton.jpg",
"O tradicional Teleton, que anualmente levanta dinheiro para as AACD " +
"(Associação de Assistência à Criança Deficiente) foi ao ar ao longo de " +
"todo este sábado (10). Pela primeira vez na sua história, o programa " +
"apresentado por Silvio Santos teve a participação de um presidente.\n\nO " +
"tradicional Teleton, que anualmente levanta dinheiro para as AACD " +
"(Associação de Assistência à Criança Deficiente) foi ao ar ao longo de " +
"todo este sábado (10). Pela primeira vez na sua história, o programa " +
"apresentado por Silvio Santos teve a participação de um " +
"presidente.\n\nDesejando sorte ao capitão reformado, também elogiou a " +
"indicação do juiz Sergio Moro como ministro da Justiça. “”Sei que o " +
"Brasil não é um peso leve. Sei que o Brasil precisa de um presidente " +
"que tenha vontade de acertar e o senhor, nas primeiras medidas que " +
"tomou, já começou acertando. Aliás, eu não vou falar aquilo que penso, " +
"mas acho que nos próximos oito anos o senhor vai ficar no nosso " +
"governo e depois nos outros oito anos, tenho a impressão, é um " +
"palpite, não sou político, mas a sua escolha do juiz Moro… então eu " +
"acho que você pode ficar oito anos, depois passando para o Moro e ele " +
"fica mais oito. Então, o Brasil vai ter 16 anos de homens com " +
"vontade de fazer o Brasil caminhar.”"
),
News(
"Moro diz que demitirá colegas envolvidos em corrupção",
"https://www.esmaelmorais.com.br/wp-content/uploads/2017/05/moro_grampo.jpg",
"O juiz Sérgio Moro disse que “provavelmente” caberá a ele a tarefa de " +
"fazer o juízo de valor sobre o envolvimento de membros do governo Jair " +
"Bolsonaro (PSL) em esquemas de corrupção.\n\nEm entrevista à Globo, no " +
"programa Fantástico, o rapaz da lava jato afirmou que não assumiria um " +
"papel de ministro da Justiça com risco de comprometer a " +
"biografia.\n\n“Não é preciso esperar as cortes de justiça proferirem o " +
"julgamento”, avisou.\n\nDe acordo com o magistrado, Bolsonaro concordou " +
"que “ninguém” seria protegido em caso de corrupção no governo.\n\n" +
"Entretanto, a reportagem do Fantástico — apresentada durante a entrevista " +
"— contraditoriamente mostrou que Moro passou a mão na cabeça do futuro " +
"chefe da Casa Civil do governo Bolsonaro, Ônyx Lorenzoni (DEM-RS).\n\n" +
"Acusado de receber dinheiro de caixa 2 na campanha eleitoral, ônyx foi " +
"perdoado pelo juiz da lava jato porque ele [deputado] teria pedido " +
"desculpas e atuado a favor da aprovação de medidas anticorrupção no Congresso."
),
News(
"Satisfeito com rodada, Felipão procura manter foco por título",
"https://www.gazetaesportiva.com/wp-content/uploads/imagem/2018/11/11/45784683682_21d2268cd5_o-1024x644.jpg",
"A cinco partidas do final do Campeonato Brasileiro, o Palmeiras " +
"detém uma vantagem de cinco pontos na liderança. O técnico Luiz Felipe " +
"Scolari ficou satisfeito com os resultados da 33ª rodada, mas procurou " +
"espantar qualquer tipo de euforia pelo título após empatar por 1 a 1 " +
"contra o Atlético-MG.\n\nO Palmeiras contabiliza 67 pontos, cinco a " +
"mais do que o Internacional, que empatou por 1 a 1 contra o Ceará. " +
"Já o Flamengo perdeu do Botafogo por 2 a 1 e ficou com os mesmos 60 " +
"pontos. Felipão foi cauteloso ao projetar o futuro, mas falou " +
"positivamente sobre a rodada neste domingo.\n\nO Palmeiras " +
"contabiliza 67 pontos, cinco a mais do que o Internacional, que " +
"empatou por 1 a 1 contra o Ceará. Já o Flamengo perdeu do Botafogo " +
"por 2 a 1 e ficou com os mesmos 60 pontos. Felipão foi cauteloso " +
"ao projetar o futuro, mas falou positivamente sobre a rodada neste " +
"domingo.\n\n“Chegamos até as semifinais da Copa Libertadores e da " +
"Copa do Brasil e não conseguimos passar. Estamos sendo superiores " +
"nessa fórmula de campeonato, mas ainda não ganhamos. Temos que manter " +
"a cabeça no lugar, são cinco pontos de diferença e um jogo apenas " +
"para diminuir a dois”, alertou."
),
News(
"São Paulo demite Diego Aguirre; Jardine assume cargo até fim do Brasileiro",
"https://conteudo.imguol.com.br/c/esporte/01/2018/09/30/diego-aguirre-tecnico-do-sao-paulo-durante-a-partida-contra-o-botafogo-1538338486338_615x300.jpg",
"Diego Aguirre não é mais técnico do São Paulo. A saída do " +
"treinador foi definida em uma reunião na tarde deste domingo, dia " +
"seguinte ao empate em 1 a 1 no clássico com o Corinthians, na " +
"Arena.\n\nParticiparam do encontro o executivo de futebol Raí, o " +
"gerente Alexandre Pássaro e o próprio Aguirre. A má atuação do São " +
"Paulo com um a mais durante todo o segundo tempo do clássico " +
"irritou a direção tricolor.\n\nSegundo nota oficial emitida pelo " +
"clube, a decisão pela saída, \"tomada em conjunto\", aconteceu \"a " +
"partir da definição entre as partes de que o contrato do treinador " +
"não seria renovado ao término da temporada\" – leia a íntegra " +
"abaixo.\n\nSem Aguirre, o auxiliar fixo André Jardine assumirá o " +
"comando do time nas cinco rodadas finais do Brasileirão. O " +
"ex-técnico das categorias de base é a primeira opção para treinar " +
"o time principal de forma efetiva em 2019, segundo apurou o " +
"GloboEsporte.com."
)
)
}
}

Classe adaptadora de notícias

Para a classe adaptadora vamos iniciar com o layout de item de notícia, segue XML /res/layout/news.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_margin="2dp"
android:layout_width="match_parent"
android:layout_height="170dp">

<ImageView
android:id="@+id/iv_banner"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"/>

<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="126dp"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:background="@drawable/item_title_background"
android:textSize="15sp"
android:textColor="@android:color/white"
android:layout_gravity="bottom|start"
android:gravity="bottom"/>
</FrameLayout>

 

Agora o arquivo drawable que permite a aplicação de um efeito gradiente como background no TextView. Segue /res/drawable/item_title_background.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

<!--
Aplicando o gradiente de preto para transparente,
iniciando na parte de baixo da View e indo para cima,
respeitando o posicionamento de 90º.
-->
<gradient
android:startColor="#000000"
android:endColor="#00000000"
android:angle="90"/>
</shape>

 

Com o layout anterior e a definição de gradiente, conseguimos o seguinte resultado:

Efeito gradiente aplicado ao item de notícia

Abaixo o simples diagrama do layout news.xml:

Diagrama do layout news.xml

Assim o código Kotlin da classe NewsAdapter:

class NewsAdapter(
private val context: Context,
private val newsList: List<News> ) :
RecyclerView.Adapter<NewsAdapter.ViewHolder>() {

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int ) : NewsAdapter.ViewHolder {

val v = LayoutInflater
.from( context )
.inflate( R.layout.news, parent, false )

return ViewHolder( v )
}

override fun onBindViewHolder(
holder: ViewHolder,
position: Int ) {

holder.setModel( newsList[ position ] )
}

override fun getItemCount(): Int {
return newsList.size
}

inner class ViewHolder( itemView: View ) :
RecyclerView.ViewHolder( itemView ),
View.OnClickListener {

val ivBanner: ImageView
val tvTitle: TextView

init {
itemView.setOnClickListener( this )

ivBanner = itemView.findViewById( R.id.iv_banner )
tvTitle = itemView.findViewById( R.id.tv_title )
}

fun setModel( news: News ) {
tvTitle.text = news.title

Picasso
.get()
.load( news.imageUrl )
.into( ivBanner )

ivBanner.contentDescription = String.format(
"%s %s",
context.resources.getString( R.string.image_of_label ),
news.title
)
}

override fun onClick( v: View ) {
val intent = Intent( context, NewsDetailsActivity::class.java )
intent.putExtra( News.KEY, newsList[ adapterPosition ] )
context.startActivity( intent )
}
}
}

Atividade principal, NewsActivity

Vamos iniciar com os arquivos XML de layout da atividade principal. Primeiro o arquivo que contém o RecyclerView principal, /res/layout/app_bar_news.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorBackground"
tools:context=".NewsActivity">

<android.support.design.widget.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:theme="@style/AppTheme.AppBarOverlay">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"/>

</android.support.design.widget.AppBarLayout>

<android.support.v7.widget.RecyclerView
android:id="@+id/rv_news"
android:layout_margin="0dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="2dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</android.support.design.widget.CoordinatorLayout>

 

Abaixo o diagrama do layout anterior:

Diagrama do layout app_bar_news.xml

Agora o XML que contém o menu gaveta e o layout anterior. Segue /res/layout/activity_news.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
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/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">

<include
layout="@layout/app_bar_news"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
android:background="@color/colorNavigation"
app:itemBackground="@drawable/nav_background"
app:itemIconTint="@drawable/nav_icon_text"
app:itemTextColor="@drawable/nav_icon_text"
app:menu="@menu/activity_news_drawer"/>
</android.support.v4.widget.DrawerLayout>

 

NavigationView tem referência a dois arquivos drawable que permitem a correta definição de estilo nos itens do menu gaveta.

O primeiro deles é o de definição de background, /res/drawable/nav_background.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

<!-- Estado "Selecionado" -->
<item
android:drawable="@color/colorPrimaryDark"
android:state_checked="true" />

<!-- Estado "Normal", não selecionado -->
<item android:drawable="@android:color/transparent" />
</selector>

 

O outro arquivo é o que contém definições para ícone e texto de item, /res/drawable/nav_icon_text.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

<!--
A ordem dos itens de um arquivo <selector> deve ser
seguida de forma estrita, pois caso contrário os efeitos
esperados não ocorrerão.
-->

<!-- Estado "Selecionado" -->
<item
android:color="@android:color/white"
android:state_checked="true" />

<!-- Estado "Normal", não selecionado -->
<item android:color="@color/colorItemNormal" />
</selector>

 

Com as definições anteriores conseguimos o estilo de item como definido em protótipo estático:

Itens menu gaveta

Agora o arquivo de itens de menu do NavigationView, /res/menu/activity_news_drawer.xml:

<?xml version="1.0" encoding="utf-8"?>
<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:checked="true"
android:icon="@drawable/ic_news"
android:title="@string/item_label_news"/>
<item
android:icon="@drawable/ic_politics"
android:title="@string/item_label_politics"/>
<item
android:icon="@drawable/ic_business"
android:title="@string/item_label_business"/>
<item
android:icon="@drawable/ic_sport"
android:title="@string/item_label_sport"/>
<item
android:icon="@drawable/ic_science_and_tec"
android:title="@string/item_label_science_and_tec"/>
<item
android:icon="@drawable/ic_heart"
android:title="@string/item_label_health"/>
<item
android:icon="@drawable/ic_entertainment"
android:title="@string/item_label_entertainment"/>
</group>
</menu>

 

Abaixo o XML de menu que permite a colocação do ícone de busca na barra de topo da atividade principal. Segue /res/menu/news.xml:

<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<item android:id="@+id/action_settings"
android:icon="@drawable/ic_search"
android:title="@string/action_search"
app:showAsAction="always"/>
</menu>

 

Então o diagrama de activity_news.xml:

Diagrama de activity_news.xml

Agora o código dinâmico de NewsActivity:

class NewsActivity :
AppCompatActivity(),
NavigationView.OnNavigationItemSelectedListener {

override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_news )
setSupportActionBar( toolbar )

val toggle = ActionBarDrawerToggle(
this,
drawer_layout,
toolbar,
R.string.navigation_drawer_open,
R.string.navigation_drawer_close
)
drawer_layout.addDrawerListener( toggle )
toggle.syncState()

nav_view.setNavigationItemSelectedListener( this )

initList()
}

private fun initList(){
rv_news.setHasFixedSize( true )

val layoutManager = GridLayoutManager( this, 2 )
rv_news.layoutManager = layoutManager

rv_news.adapter = NewsAdapter( this, Database.getNews() )
}

override fun onBackPressed() {
if( drawer_layout.isDrawerOpen(GravityCompat.START) ){
drawer_layout.closeDrawer( GravityCompat.START )
}
else{
super.onBackPressed()
}
}

override fun onCreateOptionsMenu( menu: Menu ): Boolean {
menuInflater.inflate( R.menu.news, menu )
return true
}

override fun onNavigationItemSelected( item: MenuItem ): Boolean {
/*
* Foi deixado aqui dentro somente o necessário para
* fechar o menu gaveta quando algum item for acionado.
* */
drawer_layout.closeDrawer( GravityCompat.START )
return false /* Para não mudar o item selecionado em menu gaveta */
}
}

Atividade de detalhes de notícia

Para a NewsDetailsActivity vamos iniciar com o layout de conteúdo, /res/layout/content_news_details.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:showIn="@layout/activity_news_details"
android:background="@color/colorBackground"
tools:context=".NewsDetailsActivity">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="12dp">

<TextView
android:id="@+id/tv_title"
android:layout_alignParentTop="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="22sp"
android:textColor="@color/colorText"/>

<View
android:id="@+id/v_line"
android:background="@color/colorLine"
android:layout_width="match_parent"
android:layout_height="0.7dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_title"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"/>

<LinearLayout
android:id="@+id/ll_media"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/v_line"
android:layout_marginBottom="12dp"
android:orientation="horizontal">

<ImageView
android:id="@+id/iv_banner"
android:layout_width="0dp"
android:layout_height="144dp"
android:layout_weight="1"
android:scaleType="centerCrop"
android:textSize="22sp"/>

<View
android:id="@+id/h_line"
android:layout_width="0.7dp"
android:layout_height="match_parent"
android:background="@color/colorLine"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"/>

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical">

<ImageButton
android:id="@+id/ib_fav"
android:src="@drawable/ic_fav"
android:contentDescription="@string/bt_label_fav"
android:background="@drawable/bt_fav_background"
android:layout_width="66dp"
android:layout_height="66dp"
android:layout_marginBottom="12dp"/>

<ImageButton
android:id="@+id/ib_share"
android:src="@drawable/ic_share"
android:contentDescription="@string/bt_label_share"
android:background="@drawable/bt_share_background"
android:layout_width="66dp"
android:layout_height="66dp" />
</LinearLayout>
</LinearLayout>

<TextView
android:id="@+id/tv_description"
android:layout_below="@+id/ll_media"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/colorText"/>
</RelativeLayout>
</android.support.v4.widget.NestedScrollView>

 

A seguir os arquivos drawable utilizados como background dos dois botões disponíveis em tela.

Primeiro o arquivo do botão de "favoritos", /res/drawable/bt_fav_background.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<!--
Definindo cor de background.
-->
<solid android:color="@color/colorFavButton" />

<!--
Definindo a curvatura de pontas.
-->
<corners android:radius="3dp" />
</shape>

 

O segundo é o XML de background do botão de compartilhamento, botão que receberá ação somente na segunda parte do projeto. Segue /res/drawable/bt_share_background.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<!--
Definindo cor de background.
-->
<solid android:color="@color/colorShareButton" />

<!--
Definindo a curvatura de pontas.
-->
<corners android:radius="3dp" />
</shape>

 

Com o layout e os drawables anteriores temos o seguinte resultado nos botões da tela de detalhes:

Botões com cantos arredondados

A seguir o diagrama de content_news_details.xml:

Diagrama do layout content_news_details.xml

Agora o layout principal da atividade de detalhes, /res/layout/activity_news_details.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".NewsDetailsActivity">

<android.support.design.widget.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:theme="@style/AppTheme.AppBarOverlay">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"/>

</android.support.design.widget.AppBarLayout>

<include layout="@layout/content_news_details"/>

</android.support.design.widget.CoordinatorLayout>

 

Abaixo o diagrama do layout anterior:

Diagrama do layout activity_news_details.xml

Então o código Kotlin da NewsDetailsActivity:

class NewsDetailsActivity :
AppCompatActivity() {

lateinit var news : News

override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_news_details )
setSupportActionBar( toolbar )

supportActionBar?.setDisplayHomeAsUpEnabled( true )

news = intent.getParcelableExtra( News.KEY )

/*
* Colocando dados em tela.
* */
tv_title.text = news.title
tv_description.text = news.description

Picasso
.get()
.load( news.imageUrl )
.into( iv_banner )

iv_banner.contentDescription = String.format(
"%s %s",
getString(R.string.image_of_label),
news.title
)
}

/*
* Hackcode para sempre utilizarmos o único título
* de categoria disponível no projeto de exemplo.
* */
override fun onResume() {
super.onResume()
toolbar.title = getString( R.string.item_label_news )
}
}

Adicionando o algoritmo de compartilhamento de notícia

Nesta segunda parte do projeto nossa meta é adicionar funcionalidade ao botão de compartilhamento de notícia da tela de detalhes.

Botões tela de detalhes de notícia

Serão dois algoritmos:

  • Um para quando o usuário liberou a permissão de armazenamento, assim a imagem da notícia também será compartilhada;
  • Um outro para quando o usuário não liberar a permissão de armazenamento, dessa forma somente texto será compartilhado.

Veja o fluxograma de funcionamento da lógica de compartilhamento de notícia:

Fluxograma de funcionamento da lógica de compartilhamento de notícia

Os algoritmos de compartilhamento poderão acionar para qualquer aplicativo receptor que aceite as configurações de Intent, mas o foco será em aplicativos de email, com configurações também específicas a estes.

Utilitário para obtenção de Uri de Bitmap

Precisamos de um algoritmo utilitário que será utilizado para obter a Uri do Bitmap dentro do ImageView da notícia em compartilhamento.

Esse algoritmo foi apresentado na seção Conteúdo binário remoto, logo, vamos aproveita-lo aqui.

Crie um novo pacote com o rótulo util. Neste pacote adicione um novo arquivo Kotlin, e não uma classe, com o nome Util:

/*
* Obtém o Bitmap do ImageView passado como parâmetro e
* então retorna a URI desse Bitmap.
* */
fun getBitmapImageViewUri(
context: Context,
imageView: ImageView ): Uri {

val drawable = imageView.getDrawable() as BitmapDrawable
val bitmap = drawable.bitmap

val bytes = ByteArrayOutputStream()
bitmap.compress(
Bitmap.CompressFormat.JPEG,
100,
bytes
)

val path = MediaStore.Images.Media.insertImage(
context.getContentResolver(),
bitmap,
"",
null
)

return Uri.parse( path )
}

Solicitação de permissão de armazenamento externo

Nosso segundo passo é colocar todos os códigos para a solicitação de permissão de armazenamento externo. Vamos iniciar pela inclusão da biblioteca EasyPermissions.

No Gradle Nível de Aplicativo, build.gradle (Module: app), coloque a referência em destaque e sincronize o projeto:

...
dependencies {
...
implementation 'pub.devrel:easypermissions:1.2.0'
}

 

Agora no AndroidManifest.xml adicione os <uses-permission> em destaque:

...
<manifest ...>
...
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
...
</manifest>

 

Então na atividade de detalhes de notícias, NewsDetailsActivity, adicione o código em destaque:

class NewsDetailsActivity :
AppCompatActivity(),
EasyPermissions.PermissionCallbacks {

companion object {
const val PERMISSION_STORAGE = 2456
}
...

/*
* Neste método, que está diretamente vinculado ao
* botão de compartilhamento, nós solicitaremos a
* permissão de acesso ao SDCard para que seja possível
* compartilhar também a imagem principal do artigo.
* */
fun shareNewsPermission( view: View){

EasyPermissions.requestPermissions(
PermissionRequest
.Builder(
this,
PERMISSION_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
.setRationale( R.string.permission_inform )
.build()
)
}

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray ) {

super.onRequestPermissionsResult(
requestCode,
permissions,
grantResults )

EasyPermissions.onRequestPermissionsResult(
requestCode,
permissions,
grantResults,
this )
}

override fun onPermissionsDenied(
requestCode: Int,
perms: MutableList<String> ) {

/* TODO */
}

override fun onPermissionsGranted(
requestCode: Int,
perms: MutableList<String> ) {

/* TODO */
}
}

 

Ainda nesta seção temos de adicionar o listener de clique ao botão de compartilhamento no layout /res/layout/content_news_details.xml:

...
<ImageButton
android:id="@+id/ib_share"
android:src="@drawable/ic_share"
android:contentDescription="@string/bt_label_share"
android:background="@drawable/bt_share_background"
android:layout_width="66dp"
android:layout_height="66dp"
android:onClick="shareNewsPermission" />
...

 

Assim podemos partir para os algoritmos de compartilhamento via Intent.

Compartilhamento sem imagem de notícia

Primeiro vamos ao trecho mais simples de código de compartilhamento, quando a permissão de armazenamento é negada e não há binário, imagem, como parte do Intent de compartilhamento.

Em NewsDetailsActivity adicione os métodos a seguir:

...
/*
* Método que será invocado quando a permissão de
* acesso ao SDCard não for concedida.
* */
private fun shareNewsWithoutImage(){
val intent = Intent()

intent.type = "text/plain"

shareNewsChooserIntent( intent )
}

private fun shareNewsChooserIntent( intent: Intent ){

val body = String.format(
"%s %s\n\n%s",
getString( R.string.initial_share_body ),
news.title,
news.description
)

intent.action = Intent.ACTION_SEND
intent.putExtra( Intent.EXTRA_SUBJECT, news.title )
intent.putExtra( Intent.EXTRA_TEXT, body )

if( intent.resolveActivity( packageManager ) != null ) {
val intentChooser = Intent.createChooser(
intent,
getString( R.string.chooser_title )
)

startActivity( intentChooser )
}
}
...

 

O método shareNewsChooserIntent() foi adicionado em separado, pois ele será o mesmo método também invocado para o algoritmo de compartilhamento que contém uma imagem. Com shareNewsChooserIntent() estamos evitando a repetição de código.

Agora em onPermissionsDenied() colocamos a invocação do algoritmo de compartilhamento sem a imagem da notícia:

...
override fun onPermissionsDenied(
requestCode: Int,
perms: MutableList<String> ) {

Toast
.makeText(
this@NewsDetailsActivity,
getString(R.string.share_without_image),
Toast.LENGTH_SHORT
)
.show()

shareNewsWithoutImage()
}
...

 

Foi preferível informar ao usuário que devido ao não fornecimento da permissão de armazenamento a notícia será compartilhada sem imagem.

Compartilhamento com binário, imagem de notícia

Ainda na classe NewsDetailsActivity adicione o método que permite o compartilhamento de conteúdo contendo a imagem da notícia:

...
/*
* Método que será invocado quando a permissão de
* acesso ao SDCard for concedida e assim a imagem
* principal do artigo também esta liberada para
* compartilhamento.
* */
private fun shareNewsWithImage(){
val bitmapUri = getBitmapImageViewUri(this, iv_banner)
val intent = Intent()

intent.putExtra( Intent.EXTRA_STREAM, bitmapUri )
intent.type = "image/*"

shareNewsChooserIntent( intent )
}
...

 

Então no método onPermissionsGranted() coloque a invocação para o novo método:

...
override fun onPermissionsGranted(
requestCode: Int,
perms: MutableList<String> ) {

shareNewsWithImage()
}
...

Testes e resultados

Com o Android Studio aberto vá em "Build" logo depois acione "Rebuild Project". Depois do rebuild execute o projeto em seu emulador ou aparelho de testes.

Compartilhando o conteúdo com a permissão de armazenamento ainda não concedida:

Compartilhamento somente de texto

Compartilhando o conteúdo com a permissão de armazenamento sendo concedida:

Compartilhamento de texto e imagem

Assim terminamos por completo o tutorial de como compartilhar conteúdos de aplicativos Android utilizando APIs nativas.

Não esqueça de se inscrever na 📩 lista de emails do Blog para receber em primeira mão os conteúdos exclusivos sobre desenvolvimento Android.

Se inscreva no canal do Blog em: YouTube Thiengo.

Slides

Abaixo os slides com a explicação sobre o compartilhamento Android via APIs nativas:

Vídeos

Abaixo os vídeos com o passo a passo de atualização do aplicativo de notícias para compartilhamento via APIs nativas:

Para acessar o projeto de exemplo entre no GitHub dele em: https://github.com/viniciusthiengo/brasil-news-kotlin-android.

Conclusão

Permitir o compartilhamento de conteúdos de seu aplicativo Android é o primeiro passo para conseguir atingir novos usuários.

Poder utilizar APIs nativas consistentes para isso é ainda mais eficiente, pois o APK final do app Android não fica com um tamanho exagerado. Tendo em mente que as APIs nativas de compartilhamento são, até o momento da construção deste conteúdo, ao menos tão robustas quanto as melhores APIs de terceiros.

Vale lembrar que alguns aplicativos exigirão o uso das APIs deles para que o compartilhamento seja consistente em qualquer circunstância. O Facebook Android App é um exemplo deste tipo de aplicativo.

Caso você tenha alguma dica ou dúvida sobre compartilhamento no Android, deixe nos comentários.

Se curtiu o artigo, não esqueça de compartilha-lo. E, por fim, se inscreva na 📩 lista de emails.

Abraço.

Fontes

Sending simple data to other apps

Sharing Content with Intents

FileProvider

Send Email Intent - Resposta de thanhbinh84 e Community♦

Get the URI of an image stored in drawable - Resposta de Michael

How to get the Uri of a image stored on the SDCARD? - Resposta de SeyedPooya Soofbaf

How to share text to WhatsApp from my app? - Resposta de Sonny Ng e vault

How to share text to WhatsApp from my app? - Resposta de Jitendra Kumar. Balla e Paolo Forgia

Get Bitmap from ImageView in Android L - Resposta de Pankaj Arora e Bipin Bharti

How to get a Uri object from Bitmap - Resposta de Ajay e blueware

How to filter specific apps for ACTION_SEND intent (and set a different text for each app) - Resposta de dacoinminster e Pragati Singh

Simple Android grid example using RecyclerView with GridLayoutManager (like the old GridView) - Resposta de Suragch

Transparent black gradient shape drawable color code - Resposta de Kling Klang

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes!
Email inválido

Relacionado

Utilizando Intenções Para Mapas de Alta Qualidade no AndroidUtilizando Intenções Para Mapas de Alta Qualidade no AndroidAndroid
Lottie API Para Animações no AndroidLottie API Para Animações no AndroidAndroid
Data Binding Para Vinculo de Dados na UI AndroidData Binding Para Vinculo de Dados na UI AndroidAndroid
Live Templates Para Otimização de Tempo no Android StudioLive Templates Para Otimização de Tempo no Android StudioAndroid

Compartilhar

Comentários Facebook

Comentários Blog (8)

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...
02/12/2019
Parabéns pelo material top . Thiengo ,  e muito obrigado por compartilhar conosco !! ....

Apenas uma dúvida, pode me esclarecer se você pode e qual o caminho para obter o conteúdo dinâmico de uma variável variável embutida em uma Biblioteca ".so" dentro de um APK / android de outra APP, em execução na minha APP?

Este outro material e busco é uma solução para minha necessidade acima, segue os links que acompanham:
https: //cleitonbueno.com/py ...
https://sourceware.org/libffi/
Gostaria que eu mostre apenas o caminho possibilidade para que eu siga em frente.

Grande abraço e parabéns, mais uma vez pelo conteúdo, Thiengão !!
Responder
Vinícius Thiengo (0) (0)
30/12/2019
Thiago, tudo bem?

Se não me engano eu já lhe respondi no YouTube.

Não me lembro bem?!

De qualquer forma... para acessar qualquer dado (estático ou dinâmico) de um outro aplicativo que não compartilha o mesmo ID de processo que o seu app.

Para isso, somente se o outro aplicativo lhe permite acesso aos dados dele por meio de requisição em Intent, intenção.

Ou por meio de requisição em ContentProvider.

Se o app permite isso, certamente ele terá uma documentação pública na rede mostrando como fazer.

Caso contrário, por meios legais e aceitos no sistema Android, é impossível.

Mas é aquilo, pode sim haver uma "falha" que permita esse tipo de acesso não autorizado.

No momento eu desconheço como conseguir isso sem Intent e sem ContentProvider como ponte de comunicação.

Se você consegui e quiser compartilhar aqui, sinta-se convidado.

Thiago, é isso.

Surgindo mais dúvidas, pode perguntar.

Bons estudos.

Abraço.
Responder
Ramilton Costa Gomes Junior (1) (0)
25/06/2019
Olá Vinicius, excelente tutorial. Estou com um certo problema no momento que eu compartilho o texto do meu app com o Whatsapp, quando o texto é muito grande ele corta parte do texto. Quando o texto é menor ele compartilha toda informação. Você sabe me dizer se tem algum limite(quantidade de caracter) de compartilhamento de texto? E como eu poderia resolver isso.

Abraços.
Responder
Vinícius Thiengo (0) (0)
16/08/2019
Ramilton, tudo bem?

Tem grandes chances de isso ser realmente uma limitação do WhatsApp, ele deve sim estar colocando um limite de caracteres que pode ser compartilhado de um aplicativo terceiro para o app deles.

Será preciso "vasculhar" a FAQ oficial para ver se isso realmente acontece: https://faq.whatsapp.com/en/android/28000012

Ramilton, faça um novo teste com o texto grande que esta sendo cortado. Remova dele todos os caracteres especiais e tente compartilha-lo novamente.

Pode ser também um "bug" no algoritmo do WhatsApp com algum caractere especial em especifico.

Exemplo: a palavra "coração" se tornará "coracao" em seu texto grande que vem sendo cortado.

Se realmente for isso, problema com caractere especial, então você terá de utilizar APIs UTF8 em seus textos antes de compartilha-los no WhatsApp.

Abraço.
Responder
wagner (1) (0)
21/11/2018
Excelente artigo. Uma dúvida. Ao tentar compartilhar um arquivo pdf mais um texto, se selecionado o Whatsaap, apenas o pdf foi compartilhado o texto não. Você sabe se existe algum forma de fazer isso, compartilhar texto mais binários diferentes de imagens no Whatsaap?
Responder
Vinícius Thiengo (0) (0)
21/11/2018
Wagner, tudo bem?

Como informei no artigo, alguns aplicativos têm certas limitações / restrições e acabam por ignorar alguns dados em compartilhamento.

No caso do WhatsApp, em seus testes, o texto é ignorado quando compartilhado também com um binário.

Minha primeira dica é que você tente diferentes tipos mime (image, text, ...) utilizando exatamente a mesma estrutura de compartilhamento. Isso para ver se algum tipo mime seja o suficiente para resolver o problema.

No link a seguir tem uma listagem completa de tipos mime:

https://www.freeformatter.com/mime-types-list.html

Andei pesquisando sobre compartilhamento de múltiplos tipos de conteúdos com o aplicativo do WhatsApp e acabei encontrando a seguinte discussão no StackOverflow:

https://stackoverflow.com/questions/42123025/share-image-and-text-through-intent-with-whatsapp-using-cache

A pergunta e a resposta são do mesmo usuário, que aparentemente encontrou a solução para ser possível compartilhar texto e binário, via Intent, com o WhatsApp.

Na solução dele é utilizado o modelo de código com o FileProvider, modelo que falei sobre na seção "Conteúdo binário do armazenamento externo" do artigo acima:

https://www.thiengo.com.br/como-impulsionar-o-app-android-compartilhamento-nativo#title-08

Caso os testes com os diferentes tipos mime não deem certo, faça um teste com o modelo de código do Stack Overflow indicado.

Wagner, se mesmo assim nada, volte aqui para tentarmos uma outra solução, até mesmo limitar o seu aplicativo a compartilhar somente o binário.

Abraço.
Responder
Wagner (1) (0)
26/11/2018
Blz. Thiengo.

O problema é o compartilhamento de texto mais binários diferentes de imagens. Quando compartilho uma imagem, independente do tipo dos outros binários, e um texto não ocorre o problema. Então a solução que encontrei foi: Sempre que for selecionado o texto mais arquivos(binários) e nenhum desses arquivos sejam uma imagem, eu incluo uma imagem padrão do app na lista de arquivos a serem compartilhadas. Dessa forma o texto será exibido no compartilhamento com whatsaap.
Vlw...
Responder
Vinícius Thiengo (0) (0)
26/11/2018
Wagner, show de bola.

Obrigado pela contribuição.

Abraço.
Responder