Segurança e Persistência Android com a Biblioteca Hawk

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 /Segurança e Persistência Android com a Biblioteca Hawk

Segurança e Persistência Android com a Biblioteca Hawk

Vinícius Thiengo
(4970) (14)
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ção1ª
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 trabalhar segurança e persistência em aplicativos Android. Isso utilizando, diretamente, apenas uma biblioteca: Hawk.

Dessa vez construiremos um aplicativo de "To Do List", lista de afazeres, onde será necessário salvar e criptografar as tarefas criadas pelo usuário, isso, pois a lista de afazeres de cada user é algo pessoal e privado:

Como nos últimos artigos e vídeos aqui do Blog, vamos prosseguir com a construção do aplicativo utilizando a linguagem Kotlin. Caso este seja seu primeiro contato com esta linguagem, não deixe de entrar no artigo a seguir para aprende-la: Kotlin Android, Entendendo e Primeiro Projeto.

Abaixo os tópicos que estaremos estudando:

Persistência e privacidade (segurança)

Pode ser que a princípio você não tenha visto muita importância em trabalhar segurança na persistência de itens de afazeres de um usuário, ainda mais sabendo que o aplicativo que estaremos estudando é apenas de persistência local.

Pois na verdade quando se trata de dados do usuário, dados privados dele, a segurança é importante principalmente porque não sabemos o nível de privacidade que estará entrando no software, logo, se podemos, de maneira quase que trivial, proporcionar o mínimo possível de segurança aos dados de cada usuário, devemos fazer isso.

Somente a critério de curiosidade, em 2014 um grupo de hackers conseguiu obter um pouco mais de 4 milhões de nomes de usuário, incluindo os números de telefones deles, do aplicativo Snapchat: Snapchat database hacked, 4.6m user IDs & phone nos. leaked.

Sim, sei que a fonte do link acima não é das mais confiáveis, mas a busca que fiz foi direta, e não uma genérica sobre ataques cibernéticos mais recentes. Isso, pois fiquei ciente do acontecimento na época do ocorrido.

Provavelmente existia criptografia nos dados, mas não era das mais eficientes.

Um aplicativo malicioso com acesso root ao sistema consegue obter até mesmo os dados privados de seu app.

Quando trabalhar também a segurança?

Isso, essa é a grande dúvida que tenho: quando realmente devo trabalhar a segurança em meu aplicativo?

Antes de tudo, é importante informar que eu não sou um expert em segurança. Esse teria anos, senão décadas, somente trabalhando nessa área da computação.

Mesmo assim posso, sem receio, discutir alguns pontos aqui.

O primeiro deles é a nova regra de obrigatoriedade de uso de políticas de privacidade em aplicativos que trabalham com dados sensíveis dos usuários.

Eu particularmente sempre recomendo que se utilize uma política de privacidade se o usuário puder inserir, em algum ponto qualquer, dado que identifique ele ou contas de dele. Recomendo isso para evitar a dor de cabeça de ter o aplicativo removido da Play Store.

Se o usuário pode inserir dados que o identifique, você terá de ter essa parte em suas políticas de privacidade, incluindo a descrição de como é trabalhada a segurança desses dados.

Lembrando que há softwares que geram boa parte da política de privacidade de seu aplicativo. Ou seja, você, ao menos nos primeiros releases do app, não precisará ter ao seu lado um advogado e assim investir horas, e grana, na construção de suas políticas.

O segundo ponto é que a desculpa: "Meu domínio do problema não é segurança, não posso perder tempo com isso, não no release inicial." Não tende a ter muito suporte, pois há inúmeras APIs, como a Hawk, que lhe fornecem segurança com poucas linhas de código e com uma configuração inicial padrão que já permiti o mínimo aceitável de segurança por aqueles que são profissionais da área.

Mesmo que você não esteja com muita confiança em seu projeto, fazendo com que procrastine o uso de simples bibliotecas de criptografia de dados e com isso mantenha aquela "senha e email" de usuário em seu SharedPreferences, saiba que é possível que muitas pessoas precisarem da funcionalidade de seu app.

Ok, mas qual o problema de "muitas pessoas precisarem da funcionalidade de seu app"?

Ganhando popularidade, mesmo em pouco tempo, seu aplicativo vira alvo de ataques. Como informado anteriormente: há APIs, com foco em segurança, prontas e que não vão tomar muito de seu tempo e trabalho com a lógica específica do domínio do problema de seu app.

Não duvide do "muitas pessoas precisarem da funcionalidade de seu app". Há aplicativos que apenas permitem a configuração de toques personalizados em aparelhos Moto G que têm mais de 1 milhão de downloads, estou falando de aplicativos em português!

Resumo: desde o primeiro release, ter o mínimo possível de segurança ao menos em dados armazenados, é o recomendado. Obviamente que há aplicativos que não faz sentido o uso do "mínimo de segurança", uma calculadora IMC, por exemplo.

Biblioteca Hawk

A proposta da library Hawk (Falcão) é fornecer uma maneira simples de persistir qualquer tipo de dado utilizando uma interface pública no modelo "chave-valor". Os dados são por padrão criptografados para atender a uma outra meta da API, segurança.

A seguir o diagrama das camadas de processamento da biblioteca:

O diagrama acima foi obtido na página oficial da library em: https://github.com/orhanobut/hawk.

Até o armazenamento ocorrer, primeiro é utilizada a library Gson, isso para a conversão do objeto para uma String.

Logo depois é utilizada a biblioteca Conceal do Facebook. Essa é responsável pelo passo "segurança" e tem como principais características: a velocidade de criptografia / decriptografia; e a possibilidade de trabalhar seguramente com uma grande quantidade de dados binários (imagens, vídeos, áudios e outros).

Como últimos passos, temos a serialização com as interfaces internas da library Hawk e então o "persistir" no SharedPreferences.

A obtenção dos dados armazenados sofre o processo inverso, utilizando por padrão as mesmas interfaces e bibliotecas.

Caso a imagem anterior tenha te lembrado algo complexo em programação, não se preocupe com isso, siga com o artigo para ver como a interface pública da biblioteca Hawk é simples.

Códigos de exemplo com a API Hawk

Primeiro devemos atualizar o Gradle App Level, ou build.gradle (Module: app), com a referência a última versão da biblioteca:

...
dependencies {
...
compile 'com.orhanobut:hawk:2.0.1'
}
...

 

Logo depois vem a sincronização.

Para inicializar a API é preciso um objeto de contexto, uma atividade, por exemplo:

...
Hawk.init( this ).build()
...

 

Assim, em qualquer ponto do projeto, é possível persistir os dados, até mesmo lista de objetos:

...
Hawk.put("carros_lista", carList)
...

 

A obtenção deles também é simples:

...
val carList : ArrayList<Carro> = Hawk.get("carros_lista")
...

 

Caso queira saber quantos dados, digo, chaves referenciando dados, há na base, utilize o count():

...
Log.i("log_dados", "Quantidade chaves: ${Hawk.count()}")
...

 

Para saber se uma chave-valor está presente na base administrada pela Hawk API, utilize o contains():

...
if( Hawk.contains( "carros_lista" ) ){
// TODO
}
...

 

Para deletar alguma chave e seu valor:

...
Hawk.delete( "carros_lista" )
...

 

Para zerar a base, deletar todas as chaves e valores, utilize:

...
Hawk.deleteAll()
...

 

Como informado anteriormente: a interface pública é bem simples.

Para sobrescrever as camadas Hawk

Caso você não queira seguir com alguma das implementações padrões configuradas inicialmente na Hawk API, é possível fornecer as suas próprias:

...
Hawk.init(this)
.setEncryption( CustomEncryption() )
.setConverter( CustomConverter() )
.setLogInterceptor( CustomLogInterceptor() )
.setParser( CustomParser() )
.setSerializer( CustomSerializer() )
.setStorage( CustomStorage() )
.build()
...

 

Caso não queira trabalhar com criptografia, por algum motivo, a própria biblioteca já tem uma classe que lhe permiti isso:

...
Hawk.init(this)
.setEncryption( NoEncryption() )
.build()
...

 

Sabendo que o SharedPreferences não é uma boa opção para uma base local com muitos dados, isso, pois o XML do shared é carregado por completo na memória, você pode querer utilizar um SQLite, por exemplo. Para isso terá de implementar a Interface Storagehttps://github.com/orhanobut/hawk/blob/master/hawk/src/main/java/com/orhanobut/hawk/Storage.java.

Para visualizar os códigos de todas as outras Interfaces que devem ser implementadas em caso de uso de código de camada personalizado e também para visualizar os códigos das implementações padrões, entre no package do link a seguir: https://github.com/orhanobut/hawk/tree/master/hawk/src/main/java/com/orhanobut/hawk.

Limitações

Provavelmente você deve ter desconfiado quando informei sobre a possibilidade de salvar "qualquer tipo de dado" utilizando a API Hawk, certo?

Sim, isso não é 100% verdade, pois:

  • Dados binários, incluindo Bitmap, somente são salvos se forem bem pequenos (nem mesmo KB) ou se você criar sua própria implementação de Converter substituindo assim o uso da library Gson;
  • A instância da classe NoEncryption junto ao método setEncryption() não funciona quando seu código está em Kotlin.

As limitações comentadas anteriormente, desde a criação deste artigo, estavam marcadas como issues abertas para melhoria da biblioteca.

Projeto Android de exemplo

Dessa vez nosso aplicativo de exemplo será um de "lista de afazeres", ou "to do list". Vamos assumir que estamos apenas na parte inicial do projeto e uma base local simples e com criptografia já nos seria o suficiente.

O código completo do projeto você pode estar acessando no seguinte GitHub: https://github.com/viniciusthiengo/to-do-hawk. No artigo também estaremos trabalhando e explicando todos os algoritmos do aplicativo.

No conteúdo teremos duas partes de construção do projeto.

Na primeira já deixaremos toda a interface pronta, incluindo algumas funcionalidades de lista.

Na segunda parte, já com a biblioteca Hawk configurada, é que vamos trabalhar a persistência e ordenação de itens de lista.

Com o Android Studio aberto, crie um novo projeto com uma Empty Activity e com o nome: ToDo Hawk.

Caso esteja com o Android Studio 3+ já inicie o projeto como sendo um Kotlin. Caso contrário, aplique as configurações corretas para transformar seu projeto em um projeto Kotlin, exatamente como feito em: Kotlin Android, Entendendo e Primeiro Projeto.

Ao fim desta primeira parte teremos o seguinte aplicativo:

E a seguinte estrutura de projeto: 

Configurações Gradle

A seguir as configurações do Gradle Project Level, ou build.gradle (Project: ToDoHawk):

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

allprojects {
repositories {
jcenter()
}
}

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

 

Somente os códigos necessários ao uso do Kotlin foram adicionados.

Assim as configurações do Gradle App Level, ou build.gradle (Module: app):

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

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

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:26.+'
compile 'com.android.support:design:26.+'
testCompile 'junit:junit:4.12'
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
}
repositories {
mavenCentral()
}

 

Novamente as configurações necessárias ao Kotlin foram adicionadas, adicionamos também a library de support:design para trabalhar com o RecyclerView e outras Views necessárias ao nosso layout principal. Posteriormente voltaremos a este arquivo para a adição da library Hawk.

Note que se no tempo de sua implementação do projeto houver versões mais atuais das libraries e Gradle, utilize essas mais atuais, pois o aplicativo deverá rodar sem problemas.

Configurações AndroidManifest

O arquivo AndroidManifest.xml permanece como na criação de uma novo projeto Empty Activity no AndroidStudio:

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

<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=".MainActivity"
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>
</application>
</manifest>

Configurações de estilo

Os arquivos de estilo são ainda simples, os que tiveram maiores modificações foram os de definição de estilo e de String, isso, respectivamente, devido ao uso de Toolbar e AppBar em nosso layout principal e também devido ao trabalho com Spinner de mês e dia.

Vamos iniciar com o arquivo XML de cores, /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#00BCD4</color>
<color name="colorPrimaryDark">#0097A7</color>
<color name="colorAccent">#ff9800</color>
<color name="colorLightGrey">#888888</color>
<color name="colorDarkGrey">#555555</color>
<color name="colorCheckbox">#e8e8e8</color>
</resources>

 

Em seguida o arquivo de String, /res/values/strings.xml:

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

<string-array name="durations">
<item>Duração</item>
<item>15 minutos</item>
<item>30 minutos</item>
<item>45 minutos</item>
<item>1 hora</item>
<item>1 hora e 30 minutos</item>
<item>2 horas</item>
<item>3 horas</item>
</string-array>

<string-array name="priorities">
<item>Prioridade</item>
<item>Baixa</item>
<item>Média</item>
<item>Alta</item>
</string-array>

<string-array name="months">
<item>Janeiro</item>
<item>Fevereiro</item>
<item>Março</item>
<item>Abril</item>
<item>Maio</item>
<item>Junho</item>
<item>Julho</item>
<item>Agosto</item>
<item>Setembro</item>
<item>Outubro</item>
<item>Novembro</item>
<item>Dezembro</item>
</string-array>

<string-array name="years">
<item>2017</item>
<item>2018</item>
</string-array>

<string-array name="days_28">
<item>01</item>
<item>02</item>
<item>03</item>
<item>04</item>
<item>05</item>
<item>06</item>
<item>07</item>
<item>08</item>
<item>09</item>
<item>10</item>
<item>11</item>
<item>12</item>
<item>13</item>
<item>14</item>
<item>15</item>
<item>16</item>
<item>17</item>
<item>18</item>
<item>19</item>
<item>20</item>
<item>21</item>
<item>22</item>
<item>23</item>
<item>24</item>
<item>25</item>
<item>26</item>
<item>27</item>
<item>28</item>
</string-array>

<string-array name="days_29">
<item>01</item>
<item>02</item>
<item>03</item>
<item>04</item>
<item>05</item>
<item>06</item>
<item>07</item>
<item>08</item>
<item>09</item>
<item>10</item>
<item>11</item>
<item>12</item>
<item>13</item>
<item>14</item>
<item>15</item>
<item>16</item>
<item>17</item>
<item>18</item>
<item>19</item>
<item>20</item>
<item>21</item>
<item>22</item>
<item>23</item>
<item>24</item>
<item>25</item>
<item>26</item>
<item>27</item>
<item>28</item>
<item>29</item>
</string-array>

<string-array name="days_30">
<item>01</item>
<item>02</item>
<item>03</item>
<item>04</item>
<item>05</item>
<item>06</item>
<item>07</item>
<item>08</item>
<item>09</item>
<item>10</item>
<item>11</item>
<item>12</item>
<item>13</item>
<item>14</item>
<item>15</item>
<item>16</item>
<item>17</item>
<item>18</item>
<item>19</item>
<item>20</item>
<item>21</item>
<item>22</item>
<item>23</item>
<item>24</item>
<item>25</item>
<item>26</item>
<item>27</item>
<item>28</item>
<item>29</item>
<item>30</item>
</string-array>

<string-array name="days_31">
<item>01</item>
<item>02</item>
<item>03</item>
<item>04</item>
<item>05</item>
<item>06</item>
<item>07</item>
<item>08</item>
<item>09</item>
<item>10</item>
<item>11</item>
<item>12</item>
<item>13</item>
<item>14</item>
<item>15</item>
<item>16</item>
<item>17</item>
<item>18</item>
<item>19</item>
<item>20</item>
<item>21</item>
<item>22</item>
<item>23</item>
<item>24</item>
<item>25</item>
<item>26</item>
<item>27</item>
<item>28</item>
<item>29</item>
<item>30</item>
<item>31</item>
</string-array>
</resources>

 

Sim, o XML acima é bem extenso, pois como informado no início da seção: os arrays de String serão utilizados nos Spinners do layout de criação de tarefa.

Por fim o arquivo XML de definição de estilo, /res/values/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<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>

<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>

<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />

<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

Classe de domínio

Em nosso pacote /domain somente há uma classe de domínio, ToDo:

class ToDo(
val date: Long,
val task: String,
val duration : Int,
val priority: Int) {

fun getDateFormatted(): String {
val calendar = Calendar.getInstance()
calendar.timeInMillis = date

var date = """
${getNumDate(calendar.get(Calendar.DAY_OF_MONTH))}/
${getNumDate(calendar.get(Calendar.MONTH))}/
${getNumDate(calendar.get(Calendar.YEAR))}
"""
return date.replace("\n", "").replace(" ", "").trim()
}

private fun getNumDate( num: Int )
= if( num < 10 ){
"0$num"
}
else{
"$num"
}

fun getPriorityIcon()
= if( priority == 1 ){
R.drawable.ic_priority_low
}
else if( priority == 2 ){
R.drawable.ic_priority_medium
}
else{
R.drawable.ic_priority_high
}
}

 

Os métodos getDateFormatted()getPriorityIcon() são utilizados na classe adaptadora vinculada ao RecyclerView do projeto. Isso para fornecer os dados no formato correto. Note que o atributo date contém a data de execução da tarefa, em milissegundos.

O método getNumDate() é utilizado somente para colocar "0" a frente de números de data (dia ou mês) abaixo de dez.

O trabalho com o template de String """ é possível no Kotlin para que não sejam necessárias aquelas várias concatenações de linhas, uma após a outra. Mas note que foi preciso o uso do método replace() para remover as quebras de linhas e espaços em branco, isso, pois tudo que está entre """...""" entra como dado String na propriedade.

Dialog de criação de tarefa

Para manter a divisão de responsabilidade bem definida em nosso projeto, optei por utilizar uma DialogFragment como box de criação de tarefa.

Alguns desenvolvedores poderiam optar por utilizar um simples Dialog, porém teriam de colocar toda a lógica de negócio no contexto da atividade que contém o Dialog.

Com o DialogFragment temos o próprio ciclo de vida e também uma maior independência da atividade acionadora dele.

Primeiro vamos a apresentação do layout que será utilizado no dialog, /res/layout/fragment_dialog_task.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">

<ScrollView
android:id="@+id/sv_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginBottom="22dp"
android:text="Nova tarefa"
android:textColor="@android:color/black"
android:textSize="20sp" />

<TextView
android:id="@+id/tv_conclusion_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_title"
android:layout_marginBottom="4dp"
android:text="Data conclusão"
android:textColor="@color/colorLightGrey"
android:textSize="12sp" />

<LinearLayout
android:id="@+id/ll_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_conclusion_date"
android:orientation="horizontal">

<Spinner
android:id="@+id/sp_days"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:entries="@array/days_31" />

<Spinner
android:id="@+id/sp_months"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:entries="@array/months" />

<Spinner
android:id="@+id/sp_years"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:entries="@array/years" />
</LinearLayout>

<LinearLayout
android:id="@+id/ll_dur_prio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/ll_date"
android:layout_marginTop="22dp"
android:orientation="horizontal">

<Spinner
android:id="@+id/sp_duration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:layout_weight="1"
android:entries="@array/durations" />

<Spinner
android:id="@+id/sp_priority"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:entries="@array/priorities" />
</LinearLayout>

<TextView
android:id="@+id/tv_task"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/ll_dur_prio"
android:layout_marginBottom="4dp"
android:layout_marginTop="22dp"
android:text="Tarefa:"
android:textColor="@color/colorLightGrey"
android:textSize="12sp" />

<EditText
android:id="@+id/et_task"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/tv_task"
android:hint="Informe aqui a tarefa"
android:inputType="textMultiLine"
android:maxLines="3" />
</RelativeLayout>
</ScrollView>

<Button
android:id="@+id/bt_create_task"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/sv_container"
android:layout_marginTop="8dp"
android:background="@drawable/button_round_corner"
android:padding="8dp"
android:text="Criar tarefa"
android:textColor="@android:color/white" />
</RelativeLayout>

 

Abaixo o diagrama do layout anterior:

A seguir o dialog aberto com o layout de formulário apresentado anteriormente:

Para conseguirmos o background color e o arredondamento de borda no button "CRIAR TAREFA", utilizamos como valor do atributo android:background dele o drawable /res/drawable/button_round_corner.xml:

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

<solid android:color="@color/colorAccent" />

<stroke
android:width="1dp"
android:color="@color/colorAccent" />

<padding
android:bottom="5dp"
android:left="5dp"
android:right="5dp"
android:top="5dp" />

<corners android:radius="2dp" />
</shape>

 

Assim podemos prosseguir com o código Kotlin de TaskDialogFragment:

class TaskDialogFragment :
DialogFragment(),
View.OnClickListener,
AdapterView.OnItemSelectedListener {

/*
* PARA PERMITIR O ACESSO A CHAVE DO DIALOG NA ATIVIDADE,
* SEM NECESSIDADE DE USO DE VALORES MÁGICOS (String SEM
* ESTAR EM UMA DETERMINADA PROPRIEDADE).
* */
companion object {
val KEY = "task_dialog_fragment"
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
/*
* PARA QUE SEJA REMOVIDA A BARRA DE TOPO DO DIALOG EM
* DEVICES COM O ANDROID ABAIXO DA API 21.
* */
val dialog = super.onCreateDialog(savedInstanceState)
dialog.window!!.requestFeature(Window.FEATURE_NO_TITLE)
return dialog
}

/*
* SOMENTE PARA A DEFINIÇÃO DO LAYOUT DO DIALOGFRAGMENT
* */
override fun onCreateView(
inflater: LayoutInflater?,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater!!.inflate(R.layout.fragment_dialog_task, null, false)
}

/*
* PARA QUE SEJA POSSÍVEL ACESSAR AS VIEWS COM A SINTAXE
* PERMITIDA PELO KOTLIN-ANDROID-EXTENSIONS, TEMOS DE
* UTILIZAR O onResume() NO DIALOG OU QUALQUER OUTRO
* MÉTODO DO CICLO DE VIDA DO FRAGMENT QUE VENHA DEPOIS
* DE onCreateView(), POIS CASO CONTRÁRIO, MESMO ACESSANDO
* A VIEW EM onCreateView(), SERÁ GERADA UMA NULLPOINTEREXCEPTION.
* ISSO, POIS O LAYOUT AINDA NÃO FOI INICIALIZADO.
* */
override fun onResume() {
super.onResume()
bt_create_task.setOnClickListener(this)
sp_months.setOnItemSelectedListener(this)
}

/*
* PARA QUE SEJA CRIADO UM NOVO OBJETO ToDo E ENTÃO SEJA
* ELE ENVIADO A LISTA DE ITENS VINCULADOS AO ADAPTER DO
* RECYCLERVIEW DA MainActivity.
* */
override fun onClick(view: View?) {
/*
* CONVERSÃO DE DADOS DE DATA EM STRING PARA MILLISECONDS.
* */
val calendar = Calendar.getInstance()
calendar.set(
getSelectedYear(),
sp_months.selectedItemPosition + 1,
sp_days.selectedItemPosition + 1,
0,
0,
0
)

val toDo = ToDo(
calendar.timeInMillis,
et_task.text.toString(),
sp_duration.selectedItemPosition,
sp_priority.selectedItemPosition
)

(activity as MainActivity).addToList( toDo )
dismiss()
}

/*
* PARA OBTER O VALOR CORRETO DE ANO QUE ESTÁ EM CADA ITEM DO
* Spinner DE ANOS DO FORMULÁRIO. ISSO, POIS O VALOR DO ITEM
* SELECIONADO, APENAS, É PARTINDO DE 0 (ZERO), O QUE NÃO NOS
* SERVE.
* */
private fun getSelectedYear() = (sp_years.selectedView as TextView).text.toString().toInt()

/*
* ATUALIZA O ARRAY DE DIAS VINCULADO AO Spinner DE DIAS DO
* FORMULÁRIO, A ATUALIZAÇÃO OCORRE DE ACORDO COM A MUDANÇA DE
* VALOR NO Spinner DE MÊS.
* */
override fun onItemSelected(
parentView: AdapterView<*>?,
view: View?,
position: Int,
id: Long) {

var arrayDays = getArrayDaysResource( position )
val adapter = ArrayAdapter.createFromResource(
activity,
arrayDays,
android.R.layout.simple_spinner_item )

adapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item)
sp_days.setAdapter(adapter)
}

/*
* RETORNA O RESOURCE DO ARRAY DE DIAS CORRETO DE ACORDO COM
* O VALOR DE MÊS PASSADO COMO PARÂMETRO.
* */
private fun getArrayDaysResource( month: Int )
= if( month in arrayOf(0,2,4,6,7,9,11) ){
R.array.days_31
}
else if( month in arrayOf(3,5,8,10) ){
R.array.days_30
}
else{
if( isLeapYear( getSelectedYear() ) ){
R.array.days_29
}
else{
R.array.days_28
}
}

/*
* VERIICA SE O ANO É BISSEXTO PARA A ESCOLHA DO ARRAY DE DIAS
* CORRETO NO MÊS DE FEVEREIRO.
* */
private fun isLeapYear(year: Int)
= if (year % 4 == 0) {
if (year % 100 == 0) {
year % 400 == 0
}
else{
true
}
}
else{
false
}

/*
* SOBRESCRITA OBRIGATÓRIO DE MÉTODO DEVIDO A IMPLEMENTAÇÃO DA
* INTERFACE OnItemSelectedListener. PORÉM O MÉTODO NÃO É UTILIZADO.
* */
override fun onNothingSelected(p0: AdapterView<*>?) {}
}

 

Leia os comentários na classe acima para entender o porquê de cada método, isso, pois essa classe não passará por atualizações, permanecerá como definida aqui.

Classe adaptadora

Para a classe adapter vinculada ao RecyclerView da atividade principal, vamos iniciar com a apresentação do layout de item, o /res/layout/iten_todo.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/content_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="18dp">

<ImageView
android:id="@+id/iv_ic_priority"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:contentDescription="Ícone de prioridade" />

<ImageView
android:id="@+id/iv_ic_date"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_alignTop="@+id/iv_ic_priority"
android:layout_marginEnd="4dp"
android:layout_marginLeft="22dp"
android:layout_marginRight="4dp"
android:layout_marginStart="22dp"
android:layout_toEndOf="@+id/iv_ic_priority"
android:layout_toRightOf="@+id/iv_ic_priority"
android:contentDescription="Ícone de data da tarefa"
android:src="@drawable/ic_date" />

<TextView
android:id="@+id/tv_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/iv_ic_date"
android:layout_toEndOf="@+id/iv_ic_date"
android:layout_toRightOf="@+id/iv_ic_date"
android:textColor="@color/colorLightGrey"
android:textSize="14sp"
android:textStyle="bold" />

<TextView
android:id="@+id/tv_task"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/iv_ic_priority"
android:layout_marginTop="8dp"
android:textColor="@android:color/black"
android:textSize="17sp" />

<ImageView
android:id="@+id/iv_ic_duration"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_below="@+id/tv_task"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:layout_marginTop="12dp"
android:contentDescription="Ícone de tempo de duração de tarefa"
android:src="@drawable/ic_duration" />

<TextView
android:id="@+id/tv_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/iv_ic_duration"
android:layout_toEndOf="@+id/iv_ic_duration"
android:layout_toRightOf="@+id/iv_ic_duration"
android:textColor="@color/colorLightGrey"
android:textSize="16sp"
android:textStyle="italic" />

<CheckBox
android:id="@+id/cb_done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/tv_task"
android:layout_marginTop="6dp"
android:background="@drawable/checkbox_round_corner"
android:padding="5dp"
android:text="Done"
android:textColor="@color/colorDarkGrey"
android:textSize="16sp" />
</RelativeLayout>

A seguir o diagrama do layout anterior:

E um exemplo de como ele é quando apresentado em tela:

Para conseguirmos esse background e bordas arredondadas no CheckBox, utilizamos o XML /res/drawable/checkbox_round_corner.xml (abaixo) como valor do atributo android:background dele:

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

<solid android:color="@color/colorCheckbox" />

<stroke
android:width="1dp"
android:color="@color/colorCheckbox" />

<padding
android:bottom="5dp"
android:left="5dp"
android:right="5dp"
android:top="5dp" />

<corners android:radius="2dp" />
</shape>

 

Assim podemos partir para o código Kotlin do adapter ToDoAdapter:

class ToDoAdapter(
private val context: Context,
private val toDoList: List<ToDo>) :
RecyclerView.Adapter<ToDoAdapter.ViewHolder>() {

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

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

return ViewHolder(v)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.setData(toDoList[position])
}

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

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

var ivPriority: ImageView
var tvDate: TextView
var tvTask: TextView
var tvDuration: TextView
var cbDone: CheckBox

init {
ivPriority = itemView.findViewById(R.id.iv_ic_priority)
tvDate = itemView.findViewById(R.id.tv_date)
tvTask = itemView.findViewById(R.id.tv_task)
tvDuration = itemView.findViewById(R.id.tv_duration)
cbDone = itemView.findViewById(R.id.cb_done)
cbDone.setOnCheckedChangeListener(this)
}

fun setData(toDo: ToDo) {
ivPriority.setImageResource( toDo.getPriorityIcon() )
tvDate.text = toDo.getDateFormatted()
tvTask.text = toDo.task
tvDuration.text = context.resources.getStringArray(R.array.durations)[toDo.duration]
cbDone.isChecked = false
}

override fun onCheckedChanged(checkBox: CompoundButton?, status: Boolean) {
(context as MainActivity).removeFromList( adapterPosition )
}
}
}

 

Note que quando o CheckBox é marcado o método removeFromList() é acionado na MainActivity.

De todo o código acima, a maioria é boilerplate code, ou seja, deve estar na configuração de uma classe adaptadora de RecyclerView. Somente os métodos setData()onCheckedChanged() é que são exclusivos de nossa lógica de negócio.

Ok, mas por que cbDone sempre recebe false em isChecked no método setData()?

Isso é necessário, pois em nossa lógica, caso o CheckBox seja marcado, o item deve ser removido da lista. Quando tendo muitos itens e assim seguir deletando um por um, caso o código cbDone.isChecked = false não seja utilizado, alguns itens não removidos vão aparecer com o CheckBox marcado devido a reciclagem dos layouts, algo que seria inconsistente para o usuário do app, pois o item aparecendo com o check marcado não foi selecionado por ele para ser removido.

Atividade principal

Para a atividade principal, vamos iniciar com o código XML de layout, /res/layout/activity_main.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="@android:color/white"
tools:context="br.com.thiengo.todohawk.MainActivity">

<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
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_todo"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:srcCompat="@drawable/ic_plus" />

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

 

Assim o diagrama do layout anterior:

Diagrama do layout activity_main.xml

Por fim o código Kotlin de MainActivity:

class MainActivity : AppCompatActivity() {

val list = ArrayList<ToDo>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val toolbar = findViewById(R.id.toolbar) as Toolbar
setSupportActionBar(toolbar)

fab.setOnClickListener{
initTaskDialog()
}

initRecycler()
}

private fun initRecycler() {
rv_todo.setHasFixedSize(true)

val mLayoutManager = LinearLayoutManager(this)
rv_todo.layoutManager = mLayoutManager

val divider = DividerItemDecoration(
this,
mLayoutManager.orientation)
rv_todo.addItemDecoration(divider)

val adapter = ToDoAdapter(this, list)
rv_todo.adapter = adapter
}

private fun initTaskDialog(){
val fm = supportFragmentManager
val ft = fm.beginTransaction()
val fragAnterior = fm.findFragmentByTag(TaskDialogFragment.KEY)

if (fragAnterior != null) {
ft.remove(fragAnterior)
}
ft.addToBackStack(null)

val dialog = TaskDialogFragment()
dialog.show(ft, TaskDialogFragment.KEY)
}

fun addToList(toDo: ToDo ){
list.add( toDo )
rv_todo.adapter.notifyItemInserted( list.size )
}

fun removeFromList(position: Int ){
list.removeAt( position )
rv_todo.adapter.notifyItemRemoved( position )
}
}

 

Como informado na seção de início desta primeira parte: boa parte do projeto está pronto, o que ainda falta é a persistência com criptografia e a ordenação dos itens de lista. Vamos a essas atualizações.

Integração Hawk API para evolução do aplicativo

A seguir vamos as nossas metas de evolução de aplicativo:

  • Permitir persistência de maneira simples e com criptografia, para segurança dos dados;
  • Permitir que a inserção de novos itens de tarefa já aconteça de maneira ordenada, isso com uso das propriedades date (ascendente), priority (descendente) e duration (ascendente).

Nossa persistência simples, assumindo que este é apenas o primeiro release do app, não precisa de ter um sistema de busca complexo, até porque o domínio do problema, lista de afazeres, não requer isso. Apenas uma base de chave-valor já nos atende.

Temos de ter em mente também que nosso aplicativo, mesmo tendo uma facilidade grande na criação de itens de lista, não será um daqueles onde haverá centenas de itens. A lista de afazeres é reduzida assim que as tarefas vão sendo concluídas, incluindo que os itens de "tarefas a realizar" entram na lista de acordo com os dias de uso do user ao App e não todos os itens de uma única vez, digo, este tende a ser o comportamento comum.

Com isso, apresentadas algumas regras de negócio de nosso aplicativo de To Do List, podemos seguramente continuar com a evolução do app com o uso da biblioteca Hawk.

Atualização Gradle App Level

Em build.gradle (Module: app) coloque a seguinte nova referência em destaque:

...
dependencies {
...
/* HAWK LIBRARY */
compile 'com.orhanobut:hawk:2.0.1'
}
...

 

Sempre dê prioridade a versão mais atual da API, caso em sua época tenha uma versão acima da 2.0.1, utiliza a nova versão.

Sincronize o projeto.

Inicialização da API e da lista de tarefas

Na atividade principal vamos criar um novo método responsável por inicializar a API Hawk e também por obter desta API a lista de tarefas já salva e então coloca-la na propriedade list. Segue:

class MainActivity : AppCompatActivity() {

val list = ArrayList<ToDo>()

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

initList()
initRecycler()
}

private fun initList(){
Hawk.init(this).build()

if( !Hawk.contains( "to_do_list" ) ){
Hawk.put( "to_do_list", list )
}

list.addAll( Hawk.get( "to_do_list" ) )
}
...
}

 

O algoritmo é bem intuitivo ao que deve ser feito. Mas temos um problema de código limpo no script anterior, a String "to_do_list" é um valor mágico, na maneira como está sendo utilizada, e isso é ponto negativo em um projeto de software.

Caso queiramos futuramente atualizar o nome de chave de acesso a lista de tarefas em Hawk, teremos de mudar o código em cada ponto onde tem "to_do_list", o problema fica ainda mais sério se a atualização for realizada por outro desenvolvedor que não participou da construção do aplicativo.

Para isso, evitar o uso de valor mágico, vamos colocar essa String em um local do projeto onde ela faz sentido e então acessa-la de maneira simples, como o uso de dados estáticos no Java.

Na classe ToDo acrescente:

data class ToDo(
val date: Long,
val task: String,
val duration : Int,
val priority: Int) {

companion object {
@JvmField val TO_DO_LIST_KEY = "to_do_list"
}
...
}

 

Assim, no método initList() da MainActivity, atualize:

...
private fun initList(){
Hawk.init(this).build()

if( !Hawk.contains( ToDo.TO_DO_LIST_KEY ) ){
Hawk.put( ToDo.TO_DO_LIST_KEY, list )
}

list.addAll( Hawk.get( ToDo.TO_DO_LIST_KEY ) )
}
...

 

Em caso de atualização de chave de acesso, somente a companion property de ToDo é que será atualizada.

Salvando item na base de dados

A seguir o método da MainActivity de inserção de item em lista:

...
fun addToList(toDo: ToDo ){
list.add( toDo )
rv_todo.adapter.notifyItemInserted( list.indexOf( toDo ) )
}
...

 

Esse método é invocado no onClick() da classe TaskDialogFragment.

Para que seja possível salvar a lista com o novo item, aplique a seguinte atualização:

...
fun addToList(toDo: ToDo ){
list.add( toDo )
Hawk.put( ToDo.TO_DO_LIST_KEY, list )
rv_todo.adapter.notifyItemInserted( list.indexOf( toDo ) )
}
...

 

Somente isso.

Removendo item da base de dados

Você já deve ter notado que nosso item de base de dados é na verdade uma lista com vários itens. Para a base utilizada pelo Hawk, a SharedPreferences, nós na verdade somente temos um item: a lista.

Logo, para removermos uma tarefa, temos de primeiro ter acesso a lista, remover o item dela e posteriormente salvar a lista atualizada na base via Hawk.put().

Na MainActivity, no método removeFromList(), coloque o código em destaque:

...
fun removeFromList(position: Int ){
list.removeAt( position )
Hawk.put( ToDo.TO_DO_LIST_KEY, list )
rv_todo.adapter.notifyItemRemoved( position )
}
...

 

Assim podemos prosseguir para a meta de ordenação de lista.

Ordenando itens de lista no Kotlin

Ainda temos a meta de ordenação de tarefas a ser atendida. Vamos recapitular como queremos que a ordenação da lista de tarefas ocorra:

  1. Primeiro por data de conclusão, de forma ascendente (crescente);
  2. Depois por prioridade (baixa, média e alta), de forma descendente (decrescente);
  3. E por fim por duração, de forma ascendente.

Temos de aplicar a ordenação assim que um novo item for adicionado e ainda não tenha ocorrido a atualização na base via Hawk.put() e também não tenha ocorrido a atualização do RecyclerView em tela.

Assim, o melhor local é ainda o método addToList() da MainActivity:

...
fun addToList(toDo: ToDo ){
list.add( toDo )
list.sortWith(
compareBy<ToDo>{ it.date }
.thenByDescending{ it.priority }
.thenBy{ it.duration }
)
Hawk.put( ToDo.TO_DO_LIST_KEY, list )
rv_todo.adapter.notifyItemInserted( list.size )
}
...

 

O método sortWith() é apenas uma maneira de ordenar lista no Kotlin. Ele espera como argumento um objeto do tipo Comparator, esse que é o retorno da função pública compareBy().

Na versão de compareBy() que estamos utilizando, temos de definir o tipo de dado que será utilizado nas comparações, aqui é ToDo, e logo depois, como único argumento, um Lambda indicando qual será a propriedade, ou método, utilizada na comparação. O tipo, ou retorno, dessa propriedade / método tem de ser um Int na versão de compareBy() que temos em uso.

Os outros métodos encadeados ao Lambda, thenByDescending() e thenBy(), permitem que a sequência de ordenação prossiga como informado no início da seção: date (ascendente), priority (descendente) e duration (ascendente).

Ok, mas e esse it?

O it pode ser utilizado em Lambda, no Kotlin, quando há somente um parâmetro. O que temos no código anterior com o it é equivalente a:

...
fun addToList(toDo: ToDo ){
list.add( toDo )
list.sortWith(
compareBy<ToDo>{ toDoParam -> toDoParam.date }
.thenByDescending{ toDoParam -> toDoParam.priority }
.thenBy{ toDoParam -> toDoParam.duration }
)
Hawk.put( ToDo.TO_DO_LIST_KEY, list )
rv_todo.adapter.notifyItemInserted( list.size )
}
...

 

Note que ainda temos um problema. Caso sejam criadas duas tarefas ou mais onde somente os textos de tarefas e as prioridades sejam distintas, a ordenação tende a falhar no uso da propriedade date, digo, a ordenação não será consistente tendo em mente que o usuário somente pode definir: dia, mês e ano.

Por que não será consistente?

Pois mesmo colocando a configuração inicial do objeto Calendar como a seguir:

...
calendar.set(
getSelectedYear(),
sp_months.selectedItemPosition + 1,
sp_days.selectedItemPosition + 1,
0,
0,
0
)
...

 

Mesmo assim os milissegundos são definidos pelo SO e assim nunca haverá datas iguais.

Para corrigir este problema na ordenação, vamos criar um método de conversão, na classe ToDo, de milissegundos para segundos e então utiliza-lo no lugar de date em sortWith(). Segue atualização em ToDo:

data class ToDo(...) {
...
fun getDateInSeconds() = date / 1000
}

 

E assim a atualização em addToList():

...
fun addToList(toDo: ToDo ){
list.add( toDo )
list.sortWith(
compareBy<ToDo>{ it.getDateInSeconds() }
.thenByDescending { it.priority }
.thenBy{ it.duration }
)
Hawk.put( ToDo.TO_DO_LIST_KEY, list )
rv_todo.adapter.notifyItemInserted( list.size )
}
...

 

Com isso já temos a ordenação acontecendo, porém ainda há problema, comentado e resolvido na seção seguinte.

Obtendo o posicionamento de um objeto em lista

Vamos recapitular a última linha do método addToList() da atividade principal:

...
rv_todo.adapter.notifyItemInserted( list.size )
...

 

Neste ponto a lista de tarefas já está ordenada corretamente, porém quando utilizando o método notifyItemInserted() com o tamanho da lista como argumento, list.size, o que estamos fazendo na verdade é informando ao adapter que é para atualizar somente o último item da lista. coloca-lo em tela.

Ou seja, caso o item adicionado realmente seja o último da lista atualizada, o problema não será notado, caso contrário haverá repetição de item, pois não foi na última posição da lista que adicionamos um novo item.

Segue a print de um resultado não esperado depois da adição de uma tarefa que deve vir a frente da tarefa atual em lista:

O problema é visualizado somente no momento da adição do item, pois na lista e na base a ordem está correta. Colocando a tela em landscape, temos a reconstrução da atividade e então a listagem correta:

O que precisamos é somente da posição atual do novo objeto em lista, para isso vamos utilizar o indexOf()

...
fun addToList(toDo: ToDo ){
list.add( toDo )
list.sortWith(
compareBy<ToDo>{ it.getDateInSeconds() }
.thenByDescending { it.priority }
.thenBy{ it.duration }
)
Hawk.put( ToDo.TO_DO_LIST_KEY, list )
rv_todo.adapter.notifyItemInserted( list.indexOf( toDo ) )
}
...

 

Com isso nosso problema de visualização errada de inserção está resolvido.

Note que internamente o indexOf() utiliza o equals() para comparação de objetos. Caso sua classe, do tipo do objeto em comparação, não seja uma data class, o identificador único de objeto será utilizado na comparação.

Sendo assim, se você tiver um novo objeto, porém com os mesmos valores de propriedades, e então for utilizar o indexOf(), o valor -1 será retornado, pois nenhum objeto em lista será encontrado, mesmo você sabendo que há um de mesmos valores de propriedades presente na coleção.

Em nosso caso, ao menos com a lógica de negócio atual, o objeto que é utilizado em indexOf() é exatamente o mesmo que foi colocado na lista, mas caso não fosse essa a situação e mesmo assim tivéssemos de criar um objeto para comparação, deveríamos colocar nossa classe ToDo como uma data class:

data class ToDo(...) {
...
}

 

Dessa forma o equals() utilizaria os valores das propriedades no algoritmo de comparação.

Um último problema: lista em animação

Caso existam várias tarefas realizadas porém ainda não removidas da lista, o usuário poderá ir removendo uma por uma rapidamente, apenas marcando o CheckBox de cada item.

Porém caso a remoção de um item ocorra no momento que está ocorrendo a remoção de um acionado anteriormente, provavelmente será lançada uma Exception e o aplicativo será fechado, pois foi solicitado ao RecyclerView uma animação enquanto ele ainda estava computando o novo tamanho da lista em tela.

Para criar uma proteção a isso, vamos primeiro colocar um novo método na MainActivity, onde se encontra o RecyclerView. Um método de verificação de animação neste framework de lista:

class MainActivity : AppCompatActivity() {
...

fun isRecyclerAnimationg() = rv_todo.isAnimating || rv_todo.isComputingLayout
}

 

Assim vamos utiliza-lo no onCheckedChanged() do ToDoAdapter:

...
override fun onCheckedChanged(checkBox: CompoundButton?, status: Boolean) {
val ma = (context as MainActivity)

if( !ma.isRecyclerAnimationg() ){
ma.removeFromList( adapterPosition )
}
else{
(checkBox as CheckBox).isChecked = false
}
}
...

 

No else nós voltamos o CheckBox para o status false, pois devido a animação em Thread não será possível remover o item, não no exato momento que foi solicitada essa ação.

É raro uma Exception acontecer neste caso de animação e tamanho em computação, mas ainda pode ocorrer, logo é importante este código de segurança.

Com isso podemos ir aos testes.

Testes e resultados

Com o Android Studio aberto, vá ao menu, acesse "Build" e em seguida clique em "Rebuild Project". Logo depois execute o projeto em seu device ou emulador de testes.

Crie uma tarefa de prioridade baixa e com 30 minutos de duração:

Agora crie uma nova tarefa com prioridade baixa, duração de 15 minutos e a data de execução sendo a mesma da tarefa anterior:

Ordenação funcionando como esperado. Como a última tarefa tem uma duração menor, ela foi colocada no topo da lista de afazeres.

Agora crie uma nova tarefa, com a mesma data das anteriores, porém com a prioridade alta e a duração sendo acima de 30 minutos:

Acionando "Done" na "Tarefa 2", temos:

Saindo do aplicativo e retornando a ele você notará que a lista ainda persisti.

Assim terminamos a apresentação da biblioteca Hawk e também de algumas outras características da linguagem Kotlin ainda não comentadas aqui no Blog, o método sortWith(), por exemplo.

Se inscreva na lista de emails do Blog para receber conteúdos sobre dev Android em primeira mão.

Não se esqueça também de se inscrever no canal no Blog em: YouTube Channel Thiengo Calopsita.

Vídeo com implementação passo a passo da biblioteca

A seguir o vídeo com o exemplo de implementação passo a passo da biblioteca Hawk e com explicações sobre o aplicativo de To Do List:

Para acesso ao conteúdo completo do projeto, entre no GitHub a seguir: https://github.com/viniciusthiengo/to-do-hawk.

Conclusão

Com um domínio do problema que não exija base de persistência com buscas e inserções complexas, a biblioteca Hawk pode ser a melhor escolha tendo em mente que os dados também serão salvos somente depois de criptografados.

Note que mesmo depois do teste com a API Hawk, se for descoberto que alguma camada não trabalha de maneira aceitável com seus dados, é possível implementar a Interface correta da camada e então você mesmo criar seu algoritmo de processamento daquela parte falha.

Assim o antigo pretexto de que segurança leva tempo em implementação e que os primeiros releases não exigem isso, este argumento tende a não ir muito longe devido ao uso direto ou indiretamente de bibliotecas simples e robustas como a Facebook Conceal, como faz a API Hawk, encapsulando por completo este passo dos programadores que fazem uso dela.

Caso você tenha dúvidas ou sugestões de bibliotecas e outros, deixe nos comentários, logo abaixo. E não se esqueça de se inscrever na lista de emails do Blog para obter os conteúdos exclusivos e em primeira mão.

Abraço.

Fontes

Documentação da biblioteca Hawk

Documentação da biblioteca Facebook Conceal

Kotlin Program to Check Leap Year

StackOverflow: Shared Preferences limit

StackOverflow: How to sort based on/compare multiple values in Kotlin?

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

Kotlin Android, Entendendo e Primeiro ProjetoKotlin Android, Entendendo e Primeiro ProjetoAndroid
Iniciando com Anko Kotlin. Intenções no AndroidIniciando com Anko Kotlin. Intenções no AndroidAndroid
Colocando Telas de Introdução em Seu Aplicativo AndroidColocando Telas de Introdução em Seu Aplicativo AndroidAndroid
Facilitando o Desenvolvimento de Apps Android Com a Biblioteca AndroidUtilCodeFacilitando o Desenvolvimento de Apps Android Com a Biblioteca AndroidUtilCodeAndroid

Compartilhar

Comentários Facebook

Comentários Blog (14)

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...
Maxuel Saldanha (1) (0)
19/11/2018
Meu caro, estou com uma grande dúvida, pois toda vez que insiro, uma data por exemplo, 31/01/2018, aparece 03/02/2018, e um caso mais extremo é, ao inserir 01/12/2018 e é exibido 01/00/2019. Poderia me ajudar?
Responder
Vinícius Thiengo (0) (0)
20/11/2018
Maxuel, tudo bem?

Este problema com as data está ocorrendo com o uso da API Hawk? Caso não, qual a API de armazenamento que está sendo utilizada?

Se possível, coloque no PasteBin ( https://pastebin.com/ ) o algoritmo de data que você está utilizando. Depois compartilhe aqui o link gerado. Assim é possível melhor avaliar o problema.

Abraço.
Responder
Maxuel Saldanha (1) (0)
20/11/2018
Sim, é com a Hawk, segui os seus passado nesse código, e tem feito isso
Responder
Maxuel Saldanha (1) (0)
20/11/2018
Bom dia, meu caro Thiengo, sim, estou usando esse classe que, e o código, e observando o código, não consigo ver a razão para essa ocorrências, e após, diversos testes, percebi que, esse ocorre quando o mês é de 31 dias. Se puder me ajudar a entender e corrigir
Responder
Vinícius Thiengo (0) (0)
21/11/2018
Maxuel,

Qual o formato de dado utilizado para salvar a data: Long, String, ...  ?

Abraço.
Responder
Renato (2) (0)
09/10/2018
Thiengo, posso usar essa API  para consumir web services?
Responder
Vinícius Thiengo (0) (0)
09/10/2018
Renato, tudo bem?

É possível utiliza-la para armazenar dados consumidos de algum WebService.

Para a conexão e consumo / envio do WebService você precisa de uma API específica para isso, pois a Hawk não realiza este tipo de tarefa de comunicação.

As APIs a seguir vão fazer isso para ti, comunicar com um back-end remoto:

-> Retrofit API (a mais robusta): https://www.thiengo.com.br/library-retrofit-2-no-android

-> AsyncHttp (a mais simples): http://loopj.com/android-async-http/

A documentação oficial da Retrofit API também é muito boa: https://square.github.io/retrofit/

Abraço.
Responder
Renato Souza (1) (0)
09/10/2018
Eu comprei o livro, só não recebi meus bônus
Responder
Vinícius Thiengo (0) (0)
09/10/2018
Renato, show de bola que você investiu no livro (e nos bônus).

Neste novo título, "Desenvolvedor Kotlin Android - Bibliotecas para o dia a dia", eu fui bem mais detalhista, para que o leitor evite ao máximo ter de sair do livro para entender algo.

Os bônus, como informado na página de vendas, serão entregues ao email que realizou a compra no oitavo dia após a confirmação de pagamento.

Isso para assegurar a cobertura à garantia obrigatória de sete dias do Hotmart.

Abraço.
Responder
Renato Souza (1) (0)
10/10/2018
Excelente, valeu Thiengo.
Responder
04/06/2018
Bom Dia
Thanks for the sharing this information with us it's really useful for me.
Responder
Junior (1) (0)
13/07/2017
Boa noite, na verdade é uma mais uma duvida off. Como voce faz para gerar as imagens do diagrama do layout?
Responder
Vinícius Thiengo (1) (0)
13/07/2017
Junior, tudo bem?

Utilizo o Google Drive, opção ?Desenho?. Lá coloco os retângulos, nomes e cores neles.

Note que não fico os diagramas antes do layout, mas essa é sim uma boa opção, principalmente para aqueles que gostam de trabalhar com documentos de projeto de sistema.

Em alguns artigos tenho também fluxogramas, esses também são gerados em uma ferramenta gratuita, a Draw.IO (https://www.draw.io/ ).

Abraço.
Responder
Heraldo Gama (1) (0)
12/07/2017
Bom dia,
Mais uma excelente sugestão. Só precisamos aguardar um pouco pra ver se o pessoal da LIB implementem os recursos para rodar 100% no Kotlin também.
Responder