ViewModel Android, Como Utilizar Este Componente de Arquitetura

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 /ViewModel Android, Como Utilizar Este Componente de Arquitetura

ViewModel Android, Como Utilizar Este Componente de Arquitetura

Vinícius Thiengo
(15031) (5)
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ítuloCraftsmanship Limpo: Disciplinas, Padrões e ética
CategoriaDesenvolvimento Web
Autor(es)Robert C. Martin
EditoraAlta Books
Edição
Ano2023
Páginas416
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 passo a passo, com um exemplo de aplicativo, estudar o componente de arquitetura ViewModel.

Tanto o ViewModel como todos os outros componentes de arquitetura do pacote android.arch são novos no Android. Por um longo período o que nós desenvolvedores tínhamos eram os conhecidos padrões de arquitetura que vieram bem antes desta plataforma.

Depois do estudo detalhado do ViewModel, vamos a um exemplo prático onde: trabalharemos a fácil comunicação entre fragments pertencentes a um mesmo formulário de cadastro:

Telas do aplicativo Android de notícias

Antes de prosseguir, não deixe de se inscrever na 📩 lista de e-mails do Blog, logo acima, para receber os conteúdos Android exclusivos e em primeira mão.

Abaixo os tópicos que estaremos abordando:

Componentes de arquitetura

Para a construção de qualquer software em linguagem de alto nível (Java, Kotlin, PHP, ...) é recomendada a correta separação de conceitos. Isso para facilitar, principalmente, a evolução do aplicativo.

Enquanto não havia, na documentação do Android, um passo a passo de como construir aplicativos com ao menos uma arquitetura recomendada nesta plataforma, nós desenvolvedores passamos a utilizar padrões de arquitetura que conhecidos é úteis: MVCMVP e MVVM, por exemplo.

No ano de 2017, junto a API 26 do Android, Oreo, foram liberadas APIs nativas para o trabalho de separação de conceitos com uma arquitetura recomendada para está plataforma mobile.

A arquitetura recomendada não leva nenhum nome especial, somente que ela deve ser utilizada na construção de aplicativos Android, caso nenhuma outra, já produtiva, esteja em uso, para um projeto mais eficiente, visando a evolução dele.

As novas APIs, componentes de arquitetura, são:

  • Lifecycle: para dar às classes de domínio a propriedade de conhecer o ciclo de vida de algum componente importante a elas sem necessidade de utilizar os métodos de ciclo de vida deste componente;
  • LiveData: permite o uso simples do react / observer no projeto, evitando o uso de APIs terceiras e maior dependência entre as camadas da arquitetura em uso;
  • ViewModel: representante da camada de negócio, responsável por realizar as invocações as camadas inferiores e entregar os dados corretos a camada superior, está última a camada de visualização, Activity / Fragment;
  • Room: nova API que facilita o trabalho com a camada de modelo, persistência de dados via SQLite, porém com uma interface mais simples.

Arquitetura recomendada

A seguir o diagrama da arquitetura de um aplicativo Android de exemplo, estudado na documentação da plataforma, que tem como fonte principal de dados um database remoto que necessita de uma API de conexão remota para buscar estes dados. Há também o trabalho com dados locais para o funcionamento offline:

Modelo de arquitetura Android

Neste artigo vamos discutir a API ViewModel, em artigos posteriores apresentaremos as outras APIs Android de arquitetura, digo, as APIs listadas na seção anterior.

Note que no diagrama acima as setas pretas indicam dependência, ou seja, na arquitetura recomendada não há uma camada que é ao mesmo tempo dependente e dependência da camada inferior. Dessa forma a separação de conceitos fica ainda mais enxuta.

Ressaltando que apesar da nova arquitetura publicada, na documentação está claro que não há um único modelo de arquitetura excelente para todos os domínios, logo, se a já utilizada por você estiver com uma ótima separação de conceitos e mantendo o app "clean", seguramente você pode manter o uso dessa mesma arquitetura.

Ok Thiengo, mas e as classes de domínio? Não identifiquei elas no diagrama anterior.

O diagrama anterior é genérico, mas as classes de domínio estariam vinculadas na camada de negócio, onde as principais, ligadas a modificação de dados na camada de visualização, essas estariam também vinculadas a objetos LiveData.

ViewModel

Como explicado anteriormente, o ViewModel é a principal entidade da camada de lógica de negócio da arquitetura recomendada pelo Google Android.

É nessa camada que há as invocações à APIs de busca de dados, camada de modelo, há também inicializações de algumas classes de domínio, principalmente as que têm atualizações que devem ser refletidas na camada de visualização.

Note que o máximo possível a se obter com a camada de lógica de negócio não vem somente com o uso da API ViewModel e sim com um conjunto dela com a API LiveData, está última que seria equivalente a APIs de react como: RxJava ou Agera.

A principal característica do ViewModel é a capacidade de manter dados em memória enquanto, por exemplo, há uma reconstrução da atividade a qual ele está vinculado.

A seguir um exemplo de classe ViewModel:

class SignUpViewModel: ViewModel() {

val user: User

init {
user = User()
}
}

 

E então o acesso a instância da classe acima, direto de uma atividade:

class SignUpActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sign_up)

val model = ViewModelProviders.of( this ).get( SignUpViewModel::class.java )
}
}

 

Apesar de não estar explícito na documentação, nas classes ViewModel têm de haver um construtor vazio, sem parâmetros.

Antes de prosseguir saiba que todos os códigos apresentados aqui estão na linguagem Kotlin, mas a simplicidade se mantém mesmo com o Java.

Referência API

Para poder utilizar o ViewModel, primeiro saiba que até a construção deste artigo essa era uma API em fase "alpha test", essa e todas as outras do pacote android.arch. Mesmo assim é uma API consistente e com nenhuma contraindicação, em documentação, sobre o uso em aplicativos em produção.

Alias, também não há definida uma API Android mínima, ou seja, o suporte existe para todas as versões, ao menos para as ainda em mercado, a partir da API 10, Gingerbread.

No Gradle App Level, build.gradle (Module: app), coloque a seguinte referência, ou alguma mais atual, e então sincronize:

...
dependencies {
...

/* SOMENTE SE A API 26.1+ DE SUPORTE NÃO ESTIVER SENDO UTILIZADA */
implementation "android.arch.lifecycle:runtime:1.0.0"

/* VIEW MODEL */
implementation "android.arch.lifecycle:extensions:1.0.0-beta2"
}

Ciclo de vida

O ViewModel pode ser vinculado a dois tipos de componentes Android: activity e fragment. Depois deste vinculo o objeto ViewModel somente é removido da memória caso o componente vinculado seja destruído permanentemente.

Ok, estou confuso, e sobre a principal características do ViewModel, de se manter "vivo"?

Isso persiste, a remoção total somente ocorre, por exemplo, quando a atividade vinculada passa pelo onDestroy() e o sistema sabe que não foi uma reconstrução e sim uma finalização definitiva daquela atividade.

Com uma reconstrução, ou seja, geração de uma nova atividade, esse novo objeto activity tem vinculado a ele o já existente ViewModel.

No caso do fragment o processo é exatamente o mesmo, porém o método sinalizador não é o onDestroy() e sim o onDetach().

Método sinalizador?

Sim, este é o último método de ciclo de vida, do componente vinculado ao ViewModel, ao qual as APIs internas utilizadas pelo ViewModel saberão se deve ou não manter este objeto em memória.

A seguir o diagrama, direto da documentação, apresentando o escopo do ciclo de vida de um ViewModel dentro do contexto de uma atividade:

Ciclo de vida do ViewModel

Comunicação entre fragmentos

Uma outra importante característica do ViewModel é a capacidade de permitir a fácil comunicação entre fragmentos sem que esses tenham de saber da existência um do outro.

Ou seja, facilmente podemos descartar o uso do getArguments(), EventBusLocalBroadcastManager e mais outras APIs caso a necessidade seja a comunicação entre fragmentos. 

O código com fragments seria similar ao ViewModel em activities:

class SignUpFirstPartFragment : Fragment(){
...

override fun onStart() {
super.onStart()

val signUpViewModel = ViewModelProviders
.of( activity )
.get( SignUpViewModel::class.java )
}
}

 

E então um outro fragment acessando o mesmo ViewModel:

class SignUpSecondPartFragment : Fragment(){
...

override fun onStart() {
super.onStart()

val signUpViewModel = ViewModelProviders
.of( activity )
.get( SignUpViewModel::class.java )
}
}

 

Como é o escopo da atividade pai dos fragmentos que está sendo utilizada, mesmo que um deles seja destruído, o ViewModel estará disponível aos outros.

Note que dentro da classe ViewModel você pode colocar os métodos que necessitar, somente não crie nenhuma dependência desse tipo de objeto para com objetos da camada de visualização: activities, fragments, views e outros objetos referenciados nesta camada. Caso contrário será certo o vazamento de memória, e possíveis OutOfMemoryException.

Nos dois códigos anteriores o escopo de uma atividade foi utilizado, isso para permitir a comunicação, mas seguramente, não para comunicação entre fragmentos, você pode utilizar o this de qualquer um fragment.

ViewModel vs SavedInstanceState

O ViewModel é mais efêmero do que o SavedInstanceState, ou seja, com o SavedInstanceState é possível sair do aplicativo e voltar posteriormente com ele no mesmo estado, obviamente que de acordo com o trabalho bem feito com os dados recuperados desta API.

Porém o SavedInstanceState é para pequenos dados, caso haja uma lista de objetos Carro, esses contendo ainda outros objetos, o recomendado é que não seja utilizado o SavedInstanceState devido a limitação em memória para esta API.

O ViewModel pode seguramente manter a lista de carros ao menos até o escopo de ciclo de vida dela se manter ativo (uma atividade ou fragmento). Isso sem necessidade de implementação da Interface Parcelable, um outro ponto crítico do SavedInstanceState.

Para conseguir manter a lista de carros mesmo depois de o usuário ter deixado do aplicativo, junto ao ViewModel pode ser utilizada uma persistência como a API Room.

Assim temos um resumo: ViewModel é mais temporário, porém suporta maior quantidade de dados. SavedInstanceState faz com que os dados durem por mais tempo em memória, porém suporta uma quantidade bem menor deles.

Considerações finais

Tanto o ViewModel como todas as outras APIs de componentes de arquitetura estão em versão alpha até a construção deste artigo, porém ao menos a API ViewModel, segundo meus testes, está consistente o suficiente para auxiliar qualquer projeto Android em produção.

A nova recomendação de arquitetura do Android não está invalidando, por exemplo, o uso do MVP. Está, na verdade, nos dando uma opção que deve melhor se encaixar na maioria dos domínios de problemas de aplicativos desta plataforma.

Isso, principalmente, porque essa nova arquitetura trata com segurança o controle que nós desenvolvedores não temos sobre a camada de visualização, ou controles de interface, como diz a documentação. Quem controla o ciclo de vida dos componentes Android é o próprio SO.

Mesmo sendo já indicando em Fontes, não deixe de ler por completo, depois deste post, o conteúdo do link a seguir: Guide to App Architecture. O exemplo foi muito bem construído apesar da abstração de algumas APIs em uso.

Projeto Android

Como informado no início do artigo, para melhor apresentar a API ViewModel vamos a um projeto de exemplo que simula parte de um aplicativo real em produção.

Aqui vamos trabalhar a comunicação entre fragments de um formulário de cadastro onde há três passos até a finalização dele, o cadastro. A comunicação deverá ocorrer com o uso de um objeto ViewModel.

O aplicativo é de notícias, obviamente que trabalharemos somente a parte de interface de usuário no cadastro.

Neste primeiro trecho do artigo vamos a apresentação do projeto ainda sem o ViewModel, passando por todo o código.

Depois vamos a implantação da API em estudo, alias essa parte é a mesma apresentada também em vídeo. Caso queira ter um acesso rápido ao projeto, digo, sem acessar o passo a passo que será mostrado nas próximas linhas do artigo, entre no GitHub dele em: https://github.com/viniciusthiengo/news-app-brasil-notcias.

Assim, com o Android Studio aberto, crie um novo projeto, Empty Activity, com o seguinte nome: News App - Brasil Notícias.

Ao final dessa primeira parte teremos o seguinte aplicativo:

Tela de login e tela de cadastro do app Android

E a seguinte estrutura de projeto:

Pastas e arquivos do projeto no Android Studio

Ressaltando que não iremos até a parte da efetivação do cadastro, nem mesmo com banco de dados trabalharemos. Vamos somente até a parte da comunicação entre os fragments.

Configurações Gradle

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

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

allprojects {
repositories {
google()
jcenter()
mavenCentral() /* PARA A API ROUNDEDIMAGEVIEW */
}
}

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

 

Note que devido a API externa de imagens arredondadas houve a necessidade de acrescentarmos uma fonte de busca de APIs na parte repositories de allprojects do Gradle Project Level, mais precisamente a fonte: mavenCentral().

Agora o 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 "thiengo.com.br.newsapp_brasilnotcias"
minSdkVersion 16
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 {
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support:design:26.1.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"

/* PHOTO PICKER */
implementation 'com.android.support:recyclerview-v7:26.1.0'
implementation 'me.iwf.photopicker:PhotoPicker:0.9.10@aar'
implementation 'com.nineoldandroids:library:2.4.0'
implementation 'com.github.bumptech.glide:glide:4.1.1'

/* ROUND IMAGEVIEW */
implementation 'com.makeramen:roundedimageview:2.3.0'

/* MATERIAL DIALOG */
implementation 'me.drakeet.materialdialog:library:1.3.1'
}

 

Como na versão Project Level, com a App Level temos também algumas modificações quanto ao código inicial, já referenciando as APIs que serão utilizadas inicialmente no aplicativo: PhotoPickerRoundedImageView e MaterialDialog.

Também foram removidas as referências para APIs de testes, pois não estaremos trabalhando testes aqui.

Configurações AndroidManifest

Nossa configuração inicial para o AndroidManifest.xml também tem algumas alterações em relação a um novo projeto Empty Activity. Algumas permissões e atividade foram adicionadas:

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

<!-- PhotoPicker API -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />

<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=".activity.LoginActivity"
android:screenOrientation="portrait"
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:name=".activity.SignUpActivity"
android:label="@string/title_activity_sign_up"
android:theme="@style/AppTheme.NoActionBar" />

<!-- PhotoPicker API -->
<activity
android:name="me.iwf.photopicker.PhotoPickerActivity"
android:theme="@style/Theme.AppCompat.NoActionBar" />
<activity
android:name="me.iwf.photopicker.PhotoPagerActivity"
android:theme="@style/Theme.AppCompat.NoActionBar" />
</application>

</manifest>

Configurações de estilo

Agora os arquivos em /res/values. Vamos iniciar com as configurações iniciais para o XML de definição de cores, /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#ededed</color>
<color name="colorPrimaryDark">#bbbbbb</color>
<color name="colorAccent">#37464f</color>
<color name="colorStrokeField">#8492A6</color>
<color name="colorTitleText">#000000</color>
<color name="colorLinkText">#00A6FF</color>
</resources>

 

Então o arquivo 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>
<dimen name="appbar_padding_top">8dp</dimen>
</resources>

 

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

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">News App - Brasil Notícias</string>
<string name="title_activity_sign_up">Cadastro</string>
<string name="tab_text_1">PESSOAL</string>
<string name="tab_text_2">ACESSO</string>
<string name="tab_text_3">TERMOS</string>

<string name="label_terms_and_conditions">Concordo com os termos e condições de uso.</string>
<string name="email">Email</string>
<string name="senha">Senha</string>
<string name="entrar">Entrar</string>
<string name="criar_conta">Criar conta ➙</string>
<string name="politicas_privacidade">Políticas de privacidade</string>
<string name="informe_imagem_perfil">Toque para fornecer sua foto de perfil.</string>
<string name="nome_completo">Nome completo</string>
<string name="profissao">Profissão</string>
<string name="enviar">Enviar</string>

<string name="terms_and_conditions">
É um fato conhecido de todos que um leitor se distrairá com o conteúdo
de texto legível de uma página quando estiver examinando sua diagramação.
\n\n

A vantagem de usar Lorem Ipsum é que ele tem uma distribuição normal de
letras, ao contrário de "Conteúdo aqui, conteúdo aqui", fazendo com que
ele tenha uma aparência similar a de um texto legível.
\n\n

Muitos softwares de publicação e editores de páginas na internet agora usam
Lorem Ipsum como texto-modelo padrão, e uma rápida busca por \'lorem ipsum\'
mostra vários websites ainda em sua fase de construção.
\n\n

Várias versões novas surgiram ao longo dos anos, eventualmente por acidente,
e às vezes de propósito (injetando humor, e coisas do gênero).
\n\n

Existem muitas variações disponíveis de passagens de Lorem Ipsum, mas a maioria
sofreu algum tipo de alteração, seja por inserção de passagens com humor, ou
palavras aleatórias que não parecem nem um pouco convincentes.
\n\n

Se você pretende usar uma passagem de Lorem Ipsum, precisa ter certeza de
que não há algo embaraçoso escrito escondido no meio do texto. Todos os
geradores de Lorem Ipsum na internet tendem a repetir pedaços predefinidos
conforme necessário, fazendo deste o primeiro gerador de Lorem Ipsum autêntico
da internet.
\n\n

Ele usa um dicionário com mais de 200 palavras em Latim combinado com um punhado
de modelos de estrutura de frases para gerar um Lorem Ipsum com aparência razoável,
livre de repetições, inserções de humor, palavras não características, etc.
</string>
</resources>

 

E por fim o arquivo de definição de estilos, temas, /res/values/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light">
<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.ActionBar" />

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

Classe de domínio

Teremos apenas uma classe de domínio, pacote /domain. Objetos dessa classe deverão conter os dados possíveis no formulário de cadastro:

  • Imagem de perfil (path);
  • Nome;
  • Profissão;
  • Email;
  • Senha;
  • Status dos Termos e Condições de Uso.

Segue classe User:

class User(
var email: String = "",
var password: String = "",
var name: String = "",
var profession: String = "",
var imagePath: String = "",
var statusTerms: Boolean = false )

Fragmento de cadastro de dados pessoais

Antes de prosseguirmos com os códigos, saiba que teremos três fragmentos:

  • Cadastro de dados pessoais: imagem de perfil, nome e profissão;
  • Cadastro de dados de acesso: email e senha;
  • Termos e condições de uso: confirmação de "aceito" das regras do aplicativo.

A ordem de apresentação é como descrita acima e os três serão acessados por Swipe ou por Tabs.

Para o primeiro fragmento, de dados pessoais, vamos iniciar com o layout XML dele, /res/layout/fragment_sign_up_personal.xml:

<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">

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

<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/iv_profile"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="57dp"
android:scaleType="centerCrop"
android:src="@drawable/default_profile_image"
app:riv_border_width="0dip"
app:riv_corner_radius="30dip"
app:riv_mutate_background="false"
app:riv_oval="true"
app:riv_tile_mode="clamp" />

<TextView
android:layout_width="76dp"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/iv_profile"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:layout_marginTop="36dp"
android:layout_toEndOf="@+id/iv_profile"
android:layout_toRightOf="@+id/iv_profile"
android:text="@string/informe_imagem_perfil"
android:textSize="13sp"
android:textStyle="italic" />

<EditText
android:id="@+id/et_name"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:layout_below="@+id/iv_profile"
android:layout_centerHorizontal="true"
android:layout_marginTop="75dp"
android:background="@drawable/field_radius"
android:hint="@string/nome_completo"
android:inputType="textPersonName"
android:padding="8dp"
android:paddingLeft="16dp"
android:paddingStart="16dp" />

<EditText
android:id="@+id/et_profession"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:layout_below="@+id/et_name"
android:layout_centerHorizontal="true"
android:layout_marginTop="15dp"
android:background="@drawable/field_radius"
android:hint="@string/profissao"
android:inputType="text"
android:padding="8dp"
android:paddingLeft="16dp"
android:paddingStart="16dp" />
</RelativeLayout>
</ScrollView>

 

A API RoundedImageView facilita em muito o trabalho de arredondar as bordas do ImageView. Esta API permite ainda mais customização, aqui ficaremos somente com a parte circular.

A seguir o XML de configuração de estilo de EditText que estaremos utilizando em todos os campos de entrada de texto do projeto, segue /res/drawable/field_radius.xml:

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

<solid android:color="#fff" />

<corners android:radius="3dp" />

<stroke
android:width="0.5dp"
android:color="@color/colorStrokeField" />
</shape>

 

A seguir o diagrama do layout do fragmento de dados pessoais:

Diagrama do layout fragment_sign_up_personal.xml

E então a visualização deste layout quando terminarmos a parte de cadastro por completo: 

Visualização do layout fragment_sign_up_personal.xml

Assim o código Kotlin de SignUpPersonalFragment:

class SignUpPersonalFragment :
Fragment(),
View.OnClickListener {

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {

val view = inflater.inflate(
R.layout.fragment_sign_up_personal,
container,
false)

view
.findViewById<RoundedImageView>( R.id.iv_profile )
.setOnClickListener( this )

return view
}

override fun onClick(view: View?) {
PhotoPicker.builder()
.setPhotoCount(1)
.setShowCamera(true)
.setShowGif(true)
.setPreviewEnabled(true)
.start(activity, PhotoPicker.REQUEST_CODE);
}

fun updatePhoto( imgPath: String ){
if( !imgPath.isEmpty() ){
iv_profile.setImageURI( Uri.parse( imgPath ) )
}
}
}

 

A API PhotoPicker facilita em muito o trabalho de obtenção de imagem do device, até mesmo utilizando a câmera.

Não precisamos colocar as invocações de permissão em tempo de execução, essa API já faz isso para nós quando o Android API em uso é maior ou igual a versão 23, Marshmallow.

O método updatePhoto() é invocado na atividade container dos fragmentos, SignUpActivity, que ainda vamos definir aqui. Este método é acionado dentro de onActivityResult(), um dos passos necessários para uso do PhotoPicker.

Fragmento de cadastro de dados de acesso

Iniciando pelo layout do fragmento SignUpAccessFragment, /res/layout/fragment_sign_up_access.xml:

<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">

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

<EditText
android:id="@+id/et_email"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="159dp"
android:background="@drawable/field_radius"
android:hint="@string/email"
android:inputType="textEmailAddress"
android:padding="8dp"
android:paddingLeft="16dp"
android:paddingStart="16dp" />

<EditText
android:id="@+id/et_password"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:layout_below="@+id/et_email"
android:layout_centerHorizontal="true"
android:layout_marginTop="15dp"
android:background="@drawable/field_radius"
android:hint="@string/senha"
android:inputType="textPassword"
android:padding="8dp"
android:paddingLeft="16dp"
android:paddingStart="16dp" />

</RelativeLayout>
</ScrollView>

 

Abaixo o diagrama do layout anterior:

Diagrama do layout fragment_sign_up_access.xml

E então a visualização que teremos desse layout assim que executando o aplicativo já com os códigos finalizados:

Visualização do layout fragment_sign_up_access.xml

Por fim o código Kotlin de SignUpAccessFragment:

class SignUpAccessFragment : Fragment() {

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {

val view = inflater.inflate(
R.layout.fragment_sign_up_access,
container,
false)
return view
}
}

Fragmento de termos e condições de uso

Para esse fragmento utilizamos um texto fake para ser, no visual, algo similar ao que teríamos em uma apresentação de termos e condições de uso. O texto fake foi obtido de Lorem Ipsum, que é uma excelente fonte de texto simulado quando este é necessário.

Vamos iniciar com o layout XML de SignUpTermsFragment, /res/layout/fragment_sign_up_terms.xml:

<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">

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

<android.support.v4.widget.NestedScrollView
android:id="@+id/sv_terms_and_conditions"
android:layout_width="match_parent"
android:layout_height="310dp"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:background="@drawable/field_radius"
android:padding="8dp">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/terms_and_conditions" />
</android.support.v4.widget.NestedScrollView>

<CheckBox
android:id="@+id/cb_terms"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/sv_terms_and_conditions"
android:layout_alignStart="@+id/sv_terms_and_conditions"
android:layout_below="@+id/sv_terms_and_conditions"
android:layout_marginTop="8dp"
android:text="@string/label_terms_and_conditions" />

<Button
android:id="@+id/bt_send"
android:layout_width="88dp"
android:layout_height="36dp"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginBottom="55dp"
android:background="@drawable/button_radius"
android:enabled="false"
android:text="@string/enviar"
android:textColor="@android:color/white" />
</RelativeLayout>
</ScrollView>

 

No Button estamos utilizando um drawable de estilo que na verdade estaremos utilizando em todos os botões do projeto, segue XML de /res/drawable/button_radius.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" />

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

 

A seguir o diagrama do layout do fragment de termos e condições de uso:

Diagrama do layout fragment_sign_up_terms.xml

Agora a visualização do layout de termos quando com o aplicativo em execução:

Visualização do layout fragment_sign_up_terms.xml

Assim o código Kotlin de SignUpTermsFragment:

class SignUpTermsFragment :
Fragment(),
CompoundButton.OnCheckedChangeListener,
View.OnClickListener {

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {

val view = inflater.inflate(
R.layout.fragment_sign_up_terms,
container,
false)

view
.findViewById<CheckBox>( R.id.cb_terms )
.setOnCheckedChangeListener( this )

view
.findViewById<Button>( R.id.bt_send )
.setOnClickListener( this )

return view
}

override fun onCheckedChanged(
view: CompoundButton?,
status: Boolean) {

bt_send.isEnabled = status
}

override fun onClick(view: View?) {

val layout = LayoutInflater
.from(activity)
.inflate(R.layout.dialog_personal, null, false)

MaterialDialog(activity)
.setContentView( layout )
.setCanceledOnTouchOutside(true)
.show()
}
}

 

Note que o botão "Enviar" somente é destravado depois que o usuário marca o CheckBox concordando com os termos e condições de uso, como faríamos em um app real.

Você deve ter notado também o código de listener de clique do botão "Enviar", nele temos a abertura de um dialog, mais precisamente de um MaterialDialog, alias essa é a última parte visual que teremos no projeto:

Visualização do dialog dialog_personal.xml

Abaixo o layout utilizado junto ao MaterialDialog/res/layout/dialog_personal.xml:

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

<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:text="Nome"
android:textColor="@android:color/black"
android:textSize="20sp"
android:textStyle="bold" />

<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/iv_profile"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_below="@+id/tv_name"
android:layout_centerHorizontal="true"
android:layout_marginTop="24dp"
android:scaleType="centerCrop"
android:src="@drawable/default_profile_image"
app:riv_border_width="0dip"
app:riv_corner_radius="30dip"
app:riv_mutate_background="false"
app:riv_oval="true"
app:riv_tile_mode="clamp" />

<TextView
android:id="@+id/tv_profession"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/iv_profile"
android:layout_centerHorizontal="true"
android:layout_marginTop="32dp"
android:text="Profissão"
android:textColor="@android:color/black" />
</RelativeLayout>

 

E por fim o diagrama do layout do dialog:

Diagrama do layout dialog_personal.xml

Atividade container dos fragmentos de cadastro, SignUpActivity

Vamos iniciar com o layout de SignUpActivity, /res/values/activity_sign_up.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:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_activities"
android:fitsSystemWindows="true"
tools:context="thiengo.com.br.newsapp_brasilnotcias.activity.SignUpActivity">

<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
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:layout_weight="1"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:title="@string/app_name">

</android.support.v7.widget.Toolbar>

<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<android.support.design.widget.TabItem
android:id="@+id/tabItem"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/tab_text_1" />

<android.support.design.widget.TabItem
android:id="@+id/tabItem2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/tab_text_2" />

<android.support.design.widget.TabItem
android:id="@+id/tabItem3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/tab_text_3" />

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

<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

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

 

A seguir o diagrama do layout acima:

Diagrama do layout activity_sign_up.xml

Assim podemos ir ao código do adapter do ViewPager que teremos na atividade, isso para o correto trabalho com tabs no Android.

No pacote /conf adicione a classe SectionsPagerAdapter:

/**
* Um [FragmentPagerAdapter] que retorna um fragment correspondente a
* uma das seções/tabs/pages.
*/
class SectionsPagerAdapter(activity: SignUpActivity, fm: FragmentManager):
FragmentPagerAdapter(fm) {

override fun getItem(position: Int): Fragment {
/* getItem é invocado para instanciar o fragment da tab informada. */

return when( position ){
0 -> SignUpPersonalFragment()
1 -> SignUpAccessFragment()
else -> SignUpTermsFragment()
}
}

override fun getCount(): Int {
return 3
}
}

 

Note a utilização da sintaxe when(), provavelmente é a primeira vez que a utilizamos aqui no Blog em um conteúdo também Kotlin. O when() é equivalente ao conhecido (e temido pelos evangelizadores do polimorfismo) switch(), porém um pouco mais robusto devido a algumas abstrações possíveis.

Com isso ainda temos o código Kotlin da atividade de cadastro:

class SignUpActivity : AppCompatActivity() {

/**
* O [android.support.v4.view.PagerAdapter] é que vai prover
* fragments para cada seção / tab. Nós utilizamos um
* {@link FragmentPagerAdapter} derivado, que vai manter
* em memória todos os fragments carregados. Se isso se tornar
* muito intenso na memória, pode ser uma melhor escolha
* troca-lo por [android.support.v4.app.FragmentStatePagerAdapter].
*/
private var mSectionsPagerAdapter: SectionsPagerAdapter? = null

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

getSupportActionBar()?.setDisplayHomeAsUpEnabled(true)
getSupportActionBar()?.setDisplayShowHomeEnabled(true)

/**
* Cria o adapter que vai retornar um fragment para cada uma das três
* seções da atividade
*/
mSectionsPagerAdapter = SectionsPagerAdapter(this, supportFragmentManager)

/* Configurando o ViewPager com as seções do adapter. */
container.adapter = mSectionsPagerAdapter

container
.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabs))
tabs
.addOnTabSelectedListener(TabLayout.ViewPagerOnTabSelectedListener(container))
}

override fun onStart() {
super.onStart()
toolbar.title = resources.getString(R.string.title_activity_sign_up)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
if (id == android.R.id.home) {
finish()
return true
}
return super.onOptionsItemSelected(item)
}

override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?) {

super.onActivityResult(requestCode, resultCode, data)

if (resultCode == RESULT_OK
&& requestCode == PhotoPicker.REQUEST_CODE) {

if (data != null) {
val photos = data.getStringArrayListExtra(PhotoPicker.KEY_SELECTED_PHOTOS)
val fragment = supportFragmentManager
.findFragmentByTag("android:switcher:${container.id}:${mSectionsPagerAdapter?.getItemId(0)}")
as SignUpPersonalFragment

fragment.updatePhoto( photos[0] )
}
}
}
}

 

O onStart() está sendo utilizado, pois a definição de título de Toolbar ainda no onCreate() não tem efeito algum.

E como informado na seção do fragmento de dados pessoais, o onActivityResult() está aqui devido ao trabalho com a API PhotoPicker.

Atividade principal, LoginActivity

Por fim, para a primeira parte de nosso projeto, temos a atividade principal. Vamos iniciar pelo layout, /res/layout/activity_login.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_activities"
tools:context="thiengo.com.br.newsapp_brasilnotcias.activity.LoginActivity">

<ImageView
android:id="@+id/iv_logo"
android:layout_width="180dp"
android:layout_height="165dp"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="110dp"
android:scaleType="fitCenter"
android:src="@drawable/logo_login" />

<EditText
android:id="@+id/et_email"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:layout_below="@+id/iv_logo"
android:layout_centerHorizontal="true"
android:layout_marginTop="40dp"
android:background="@drawable/field_radius"
android:hint="@string/email"
android:inputType="textEmailAddress"
android:padding="8dp"
android:paddingLeft="16dp"
android:paddingStart="16dp" />

<EditText
android:id="@+id/et_password"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:layout_below="@+id/et_email"
android:layout_centerHorizontal="true"
android:layout_marginTop="15dp"
android:background="@drawable/field_radius"
android:hint="@string/senha"
android:inputType="textPassword"
android:padding="8dp"
android:paddingLeft="16dp"
android:paddingStart="16dp" />

<Button
android:id="@+id/bt_entrar"
android:layout_width="88dp"
android:layout_height="36dp"
android:layout_alignEnd="@+id/et_password"
android:layout_alignRight="@+id/et_password"
android:layout_below="@+id/et_password"
android:layout_marginTop="15dp"
android:background="@drawable/button_radius"
android:text="@string/entrar"
android:textColor="@android:color/white" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/et_password"
android:layout_alignStart="@+id/et_password"
android:layout_alignTop="@+id/bt_entrar"
android:onClick="callSignUp"
android:text="@string/criar_conta"
android:textColor="@color/colorLinkText" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="16dp"
android:text="@string/politicas_privacidade"
android:textColor="@color/colorLinkText" />
</RelativeLayout>

 

Abaixo o diagrama do layout anterior:

Diagrama do layout activity_login.xml

Note que todas as imagens estão disponíveis no GitHub do projeto.

Abaixo o print do layout acima quando com o aplicativo em execução:

Visualização do layout activity_login.xml

A seguir o código Kotlin de LoginActivity:

class LoginActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
}

fun callSignUp( view: View ) {
startActivity(Intent(this, SignUpActivity::class.java))
}
}

 

Simples, certo? Com isso podemos partir para a criação da comunicação entre os fragmentos utilizando a API ViewModel.

ViewModel para a comunicação entre fragmentos de cadastro

A partir deste ponto nossa meta é fazer com que seguramente os dados de cadastro dos três fragmentos de SignUpActivity possam permanecer intactos independente do estado de qualquer um dos fragmentos.

Digo isso, pois sabemos que não temos total controle sobre o ciclo de vida de qualquer um dos fragmentos e por isso, subitamente, algum deles pode ser reconstruído e assim os dados informados podem ou não serem resetados.

Com o ViewModel vamos evitar isso, utilizando como escopo a SignUpActivity, dessa forma todos os fragmentos acessarão o mesmo objeto ViewModel.

Adicionando a referência de API

No Gradle App Level, ou build.gradle (Module: app), adicione a seguinte referência em destaque:

...
dependencies {
...

/* VIEW MODEL */
implementation "android.arch.lifecycle:extensions:1.0.0-beta2"
}

 

Logo depois sincronize o projeto. Não vamos colocar a referência a 'android.arch.lifecycle:runtime', pois já temos em uso a suporte library acima da versão 26.1.

Classe ViewModel de comunicação

Nossa classe ViewModel terá o rótulo SignUpViewModel, junto teremos um objeto User para conter os dados dos campos dos fragmentos. Crie essa nova classe em um novo pacote, /logic:

class SignUpViewModel: ViewModel() {

val user: User

init {
user = User()
}
}

 

O bloco init está sendo utilizado, pois temos de manter um construtor sem parâmetros. Este é apenas um caminho para mantermos esse construtor vazio.

Nossa estratégia a partir dos fragmentos será:

  • Primeiro definir em qual método do ciclo de vida nós obteremos os dados dos campos do fragmento atual:
    • onPause(): pois é o primeiro método invocado assim que a atividade / fragmento é posto em segundo plano.
  • E então definir qual método do ciclo de vida colocaremos os dados, presentes no ViewModel, nos campos do layout do fragmento atual:
    • onStart(): aqui devido a uma API em especial. Por estarmos utilizando o Kotlin Extensions, onde podemos evitar o uso do findViewById(), optamos pelo onStart(), pois ele é um dos métodos que vêm depois do onCreateView(), este último não permite o uso do Kotlin Extensions (ao menos até a criação deste artigo), pois o layout ainda não foi enviado para ser anexado ao fragmento.

Lembre de não colocar dentro do objeto ViewModel nenhuma referência a camada de visualização (activity, fragment ou qualquer outro objeto de origem dessa camada), isso para não termos problemas de retenção de objetos e consequentemente memory leak.

ViewModel no fragmento de dados pessoais

Para o fragment SignUpPersonalFragment primeiro devemos construir um algoritmo em SignUpViewModel para preencher os dados de User somente presentes neste passo do cadastro.

Logo, em nosso novo ViewModel, adicione o seguinte código em destaque:

class SignUpViewModel: ViewModel() {
...

fun updatePersonalData(
imagePath: String,
name: String,
profession: String ){

user.imagePath = imagePath
user.name = name
user.profession = profession
}
}

 

Assim podemos partir para o código do fragmento. A seguir os novos trechos, em destaque:

class SignUpPersonalFragment :
Fragment(),
View.OnClickListener {

var imgPath: String = ""
var signUpViewModel: SignUpViewModel? = null

...

fun updatePhoto( imgPath: String ){
if( !imgPath.isEmpty() ){
this.imgPath = imgPath
iv_profile.setImageURI( Uri.parse( imgPath ) )
}
}


override fun onStart() {
super.onStart()

signUpViewModel = ViewModelProviders
.of(activity)
.get(SignUpViewModel::class.java)

/**
* COM A LINHA DE CÓDIGO ABAIXO FOI REMOVIDO O findViewById()
* do onCreateView().
*/
iv_profile.setOnClickListener(this)

updatePhoto( signUpViewModel?.user?.imagePath ?: "" )
et_name.setText( signUpViewModel?.user?.name )
et_profession?.setText( signUpViewModel?.user?.profession )
}

override fun onPause() {
super.onPause()

signUpViewModel?.updatePersonalData(
imgPath,
et_name.text.toString(),
et_profession.text.toString() )
}
}

 

Veja que foi necessária a adição de uma propriedade, imgPath, para podermos facilmente acessar o path da imagem no método onPause().

Foi inevitável manter signUpViewModel como propriedade e ao mesmo tempo podendo ser null, mesmo sabendo que isso não vai ocorrer, pois ViewModelProviders.of(activity).get(SignUpViewModel::class.java) retorna o objeto desejado, no estado esperado, de forma síncrona.

Vamos aguardar a possibilidade de, futuramente no Kotlin, definirmos um método qualquer para ser o bloco de inicialização de nossas propriedades.

ViewModel no fragmento de dados de acesso

Como no fragmento anterior, temos primeiro que definir um método em SignUpViewModel que receberá os dados do fragmento de dados de acesso:

class SignUpViewModel: ViewModel() {
...

fun updateAccessData(email: String, password: String){
user.email = email
user.password = password
}
}

 

Assim as novas configurações em SignUpAccessFragment:

class SignUpAccessFragment : Fragment() {

var signUpViewModel: SignUpViewModel? = null

...

override fun onStart() {
super.onStart()

signUpViewModel = ViewModelProviders
.of(activity)
.get(SignUpViewModel::class.java)

et_email.setText( signUpViewModel?.user?.email )
et_password.setText( signUpViewModel?.user?.password )
}

override fun onPause() {
super.onPause()

signUpViewModel?.updateAccessData(
et_email.text.toString(),
et_password.text.toString() )
}
}

 

Vale ressaltar que caso realize os testes sem uso do ViewModel, notará que ao menos nos campos de texto os conteúdos serão mantidos, isso, pois o FragmentPageAdapter mantém (tenta ao máximo) os fragmentos em memória, mantendo também os estados deles.

Porém, como informado anteriormente, nós não temos controle sobre a continuidade do ciclo de vida de fragments, activities e outros, logo o ViewModel nos dá a certeza sobre manter os dados já preenchidos, mesmo que algum fragmento tenha de ser reconstruído do zero devido a algum problema de falta de memória, por exemplo.

ViewModel no fragmento de termos de uso

Neste fragmento temos uma parte extra em relação aos anteriores, aqui teremos de colocar alguns dos dados presentes no ViewModel dentro do layout do MaterialDialog.

Primeiro vamos ao convencional: criar o método em SignUpViewModel que receberá os dados de campos do SignUpTermsFragment, no caso o estado do CheckBox.

Segue:

class SignUpViewModel: ViewModel() {
...

fun updateTermsData(terms: Boolean){
user.statusTerms = terms
}
}

 

Agora a parte convencional em SignUpTermsFragment:

class SignUpTermsFragment :
Fragment(),
CompoundButton.OnCheckedChangeListener,
View.OnClickListener {

var signUpViewModel: SignUpViewModel? = null

...

override fun onStart() {
super.onStart()

signUpViewModel = ViewModelProviders
.of(activity)
.get(SignUpViewModel::class.java)

/**
* COM AS LINHAS DE CÓDIGO ABAIXO FORAM REMOVIDOS OS findViewById()
* DO onCreateView()
*/
cb_terms.setOnCheckedChangeListener(this)
bt_send.setOnClickListener(this)

onCheckedChanged(null, signUpViewModel?.user?.statusTerms ?: false)
}

override fun onPause() {
super.onPause()

signUpViewModel?.updateTermsData( cb_terms.isChecked )
}
}

 

Agora o que resta é a obtenção dos dados de nome, profissão e imagem que estão no ViewModel para coloca-los no dialog. Segue:

class SignUpTermsFragment :
Fragment(),
CompoundButton.OnCheckedChangeListener,
View.OnClickListener {
...

override fun onClick(view: View?) {

val layout = LayoutInflater
.from(activity)
.inflate(R.layout.dialog_personal, null, false)

val tvName = layout.findViewById<TextView>( R.id.tv_name )
tvName.text = signUpViewModel?.user?.name

val tvProfession = layout.findViewById<TextView>( R.id.tv_profession )
tvProfession.text = signUpViewModel?.user?.profession

val uriImg = Uri.parse( signUpViewModel?.user?.imagePath )
val ivProfile = layout.findViewById<ImageView>( R.id.iv_profile )
ivProfile.setImageURI( uriImg )

MaterialDialog(activity)
.setContentView( layout )
.setCanceledOnTouchOutside(true)
.show()
}
...
}

 

Assim, com o código finalizado, podemos realizar alguns testes.

Testes e resultados

Dando um rebuild no projeto, Menu / Build / Rebuild Project, e executando ele, temos:

Exemplo do aplicativo Android de notícias

Rotacionando a tela você notará que nada, já preenchido, é perdido.

Com isso terminamos a apresentação da API ViewModel.

Não deixe de se inscrever na 📩 lista de e-mails do Blog, logo acima ou ao lado, para receber em primeira mão os conteúdos exclusivos sobre dev Android.

Se inscreva também no canal do Blog em: YouTube Vinícius Thiengo.

Slides

Abaixo os slides com a apresentação completa da API ViewModel:

Vídeo

Abaixo o vídeo com a implementação passo a passo da comunicação entre fragmentos utilizando a API ViewModel:

Para acesso ao projeto completo, entre no GitHub a seguir: https://github.com/viniciusthiengo/news-app-brasil-notcias.

Conclusão

Com as novas APIs de componentes de arquitetura, o desenvolvimento de aplicativos Android mais robustos, ao menos na estrutura, tende a se tornar algo comum.

Sem receios é possível dizer que a luta entre quais APIs, externas, MVP ou MVVM utilizar tende a diminuir, pois agora temos APIs nativas que podem trabalhar junto a aplicação de qualquer padrão de arquitetura.

O ViewModel é robusto mesmo quando não tendo algum LiveData sendo utilizado. Logo, vale o estudo se realmente é viável manter o Parcelable e cia. quando somente um ViewModel poderia reter, pelo ciclo necessário, os dados em memória.

Sempre assegure-se de não referenciar no ViewModel alguma entidade de origem na camada de visualização, pois está API será retida em memória em alguns momentos onde uma atividade, ou fragmento, não será e precisará ser removida para a construção uma nova atividade, ou fragmento.

Não esqueça de se inscrever na 📩 lista de e-mails e de deixar um comentário sobre o que achou.

Abraço.

Fontes

Guide to App Architecture

Adding Components to your Project

Handling Lifecycles

Documentação LiveData

Documentação ViewModel

How to use ViewModel

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

MVP AndroidMVP AndroidAndroid
Kotlin Android, Entendendo e Primeiro ProjetoKotlin Android, Entendendo e Primeiro ProjetoAndroid
Como Criar Protótipos AndroidComo Criar Protótipos AndroidAndroid
Freelancer AndroidFreelancer AndroidAndroid

Compartilhar

Comentários Facebook

Comentários Blog (5)

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...
23/11/2017
Thiengo, qual lib de picker de imagem vc recomenda? Essa mesma, PhotoPicker?
Responder
Vinícius Thiengo (0) (0)
23/11/2017
Magno, tudo bem?

Eu escolho com frequência as APIs mais simples, logo a PhotoPicker é sim uma escolha que recomendo.

Mas se quiser conhecer ainda outras, acesse o link a seguir, do Android-Arsenal: https://android-arsenal.com/tag/157?sort=rating

Abraço.
Responder
19/10/2017
Muito bom, meu amigo! Com certeza o amadurecimento dessa API de arch vai alcançar muitos seguidores. Obrigado e parabéns pelo conteúdo.
Responder
18/10/2017
Ô loco.... Thiengo, seus artigos são sempre assim? Acabei de me cadastrar na sua lista e por isso a pergunta... Meu, muito detalhado... show de bola... gostei demais! Preciso agora tirar um tempinho para ler com calma o seu artigo!

Continue assim que você terá cada vez mais e mais seguidores...

Abraço!
Responder
Vinícius Thiengo (2) (0)
18/10/2017
Reyes, tudo bem?

Hoje os artigos são todos assim, mas a tendência é que evoluam, pois inicialmente era somente vídeo. O feedback de quem acompanha é importante para permitir a evolução deles.

Sobre o artigo acima, não deixe mesmo de ler, pois esses conteúdos de arquitetura são importantes para desenvolvedores Android de qualquer nível.

Abraço.
Responder