In questa lezione su Android, mettiamo in pratica tutti i concetti visti sinora su Android Architecture Components.
Quello che vogliamo fare è creare un'app che continua a fornire in tempo reale il valore aggiornato di un titolo azionario. Questa l'interfaccia:
Il numero che appare rappresenta la quotazione in euro ma non è reale, viene infatti
generato causalmente da un oggetto di classe GeneratoreQuotazione ad intervalli di pochi secondi.
Vogliamo che tale valore prodotto rimanga a disposizione dell'interfaccia senza andare perso al di là di ogni
cambiamento di fase del ciclo di vita, anche in presenza di cambi di configurazione come rotazione del dispositivo.
Per fare ciò, utilizziamo un ViewModel, oggetto, come sappiamo, destinato a conservare i dati necessari all'interfaccia utente durante
tutta la vita del componente che la contiene.
Considerando che ogni volta che un nuovo dato viene prodotto dobbiamo aggiornare l'interfaccia utente, conserviamo il valore in un oggetto Quotazione, gestito con LiveData.
Generazione casuale dei valori
I dati di esempio vengono generati ogni volta che si invoca il metodo genera()
della classe GeneratoreQuotazioni. Il funzionamento consiste unicamente nel produrre un nuovo oggetto Quotazione che rappresenta la variazione rispetto all'ultimo prezzo calcolato di una percentuale, compresa tra -5% e +5%, generata casualmente.
public class GeneratoreQuotazioni
{
// conservato per calcolare nuovo valore in base a scostamento generato
private double valorePrecedente = 100;
private DecimalFormat df = new DecimalFormat("#.00");
private SimpleDateFormat ddf = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
public Quotazione genera()
{
return new Quotazione();
}
class Quotazione
{
private Calendar dataora;
private double quotazione;
Quotazione()
{
dataora=Calendar.getInstance();
// genera casualmente lo scostamento dal valore precedente
double var = Math.random() * 10 - 5;
quotazione = valorePrecedente * (1 + var/100);
/* quotazione attuale viene calcolata per produrre un nuovo
valore alla prossima iterazione */
valorePrecedente = quotazione;
}
String getQuotazione()
{
return df.format(quotazione);
}
String getDataora()
{
return ddf.format(dataora.getTime());
}
}
}
Il ViewModel
Il ViewModel attiva nel costruttore un Timer che ogni otto secondi interroga il GestoreQuotazioni per ottenere un nuovo valore. Il risultato ricavato viene inserito in un LiveData condiviso con l'Activity. Ogni volta che il LiveData riceverà un valore nuovo, esso attiverà (tramite Observer) il metodo che si occuperà di aggiornare l'interfaccia utente. Di seguito il codice del ViewModel che abbiamo implementato:
public class StockViewModel extends ViewModel {
private static final int INTERVALLO = 8000;
private MutableLiveData<GeneratoreQuotazioni.Quotazione> ultimaQuotazione = new MutableLiveData<>();
public StockViewModel() {
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
GeneratoreQuotazioni generatore=new GeneratoreQuotazioni();
@Override
public void run() {
GeneratoreQuotazioni.Quotazione nuovo=generatore.genera();
ultimaQuotazione.postValue(nuovo);
}
}, 0, INTERVALLO);
}
public LiveData<GeneratoreQuotazioni.Quotazione> getStockValue() {
return ultimaQuotazione;
}
}
Come si vede, il costruttore crea il Timer che attiva la produzione dei dati simulati. È Importante che il ViewModel abbia sempre un costruttore senza parametri, perché il framework dovrà essere in grado di crearne istanze in autonomia.
LiveData in dettaglio
Apriamo una breve parentesi sui LiveData. Ricapitolando, si tratta di oggetti observable (tramite
Observer possiamo rilevarne istantaneamente i cambiamenti) ma coscienti del ciclo di vita dell'Activity. LiveData possiede metodi che riguardano per lo più due tematiche principali:
- gestione dell'Observer: in particolare, con
observe()
colleghiamo un nuovo Observer (possono essere più di uno) e conremoveObserver()
ne rimuoviamo uno; - gestione dei dati: con
getValue()
possiamo estrarre il valore dal LiveData, mentre per aggiornare il suo valore abbiamo a disposizionesetValue()
epostValue()
, che differiscono in quanto il primo aggiorna il LiveData dallo stesso thread mentre il secondo è adatto per farlo da un altro thread.
Una volta compreso lo scopo dei LiveData il loro funzionamento risulta semplice, ma non sfugga la differenza
spiegata tra setValue()
e postValue()
: tale è il motivo per cui nel nostro esempio useremo il secondo metodo per spedire dati dal TimerTask (che lavora su thread secondario) all'Activity (che opera, come sempre, sul thread primario).
L'Activity
Nel metodo onCreate()
dell'Activity, seguiamo tre passi principali:
- creazione del ViewModel: non lo inizializziamo con un
new
, ma lasciamo che la classeViewModelProviders
si occupi della sua gestione; - definizione di un Observer: creiamo un Observer che venga attivato ogni volta che il LiveData
che controlla viene modificato. Ciò avverrà ogni otto secondi con il lavoro svolto dal Timer; - attivazione dell'Observer: a questo punto l'Observer appena creato viene attivato.
I passi appena elencati sono segnalati con commenti nel codice che segue:
public class MainActivity extends AppCompatActivity{
private StockViewModel mViewModel;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 1. Creazione del ViewModel
mViewModel =ViewModelProviders.of(this).get(StockViewModel.class);
// 2. definizione di un observer che rileva ogni cambiamento del LiveData
final Observer<GeneratoreQuotazioni.Quotazione> observer = new Observer<GeneratoreQuotazioni.Quotazione>() {
@Override
public void onChanged(@Nullable final GeneratoreQuotazioni.Quotazione v) {
((TextView) findViewById(R.id.valore)).setText(v.getQuotazione());
((TextView) findViewById(R.id.dataora)).setText(v.getDataora());
}
};
// 3. attivazione dell'observer sul LiveData
mViewModel.getStockValue().observe(this, observer);
}
}
Per concludere il codice dell'esempio, riportiamo anche l'XML del layout:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:layout_centerHorizontal="true"
android:orientation="vertical">
<TextView
android:layout_width="150dp"
android:layout_height="wrap_content"
android:textSize="15sp"
android:textColor="@color/colorPrimary"
android:text="Quotazione aggiornata (valori in Euro)"
android:textAlignment="center"/>
<TextView
android:layout_width="150dp"
android:layout_height="wrap_content"
android:textSize="40sp"
android:textColor="@color/colorPrimary"
android:text="0"
android:textAlignment="center"
android:id="@+id/valore"/>
<View
android:layout_width="match_parent"
android:layout_height="5dp"
android:background="@color/colorPrimary"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
/>
<TextView
android:layout_width="150dp"
android:layout_height="wrap_content"
android:textSize="15sp"
android:textColor="@color/colorPrimary"
android:textAlignment="center"
android:id="@+id/dataora"/>
</LinearLayout>
</RelativeLayout>
Conclusioni
Con ViewModel e LiveData abbiamo gettato le basi per un'architettura moderna e robusta di un'applicazione,
mantenendo sempre un occhio di riguardo al ciclo di vita. Con le prossime lezioni vedremo come sfruttarla per veicolare dati provenienti da database e Rete verso l'interfaccia utente.