Una delle novità più interessanti per gli sviluppatori Android fra quelle presentate al Google I/O 2015 è senza dubbio il framework di Data Binding. Ne abbiamo già parlato in un , adesso è arrivato il momento di provare questo framework in un esempio reale.
Nel sito ufficiale e in vari blog a giro per la rete ci sono alcuni esempi di utilizzo del Data Binding in applicazioni Android, molti di questi esempi sono fatti partendo da cosa mette a disposizione il framework (e per questo mettono in evidenza le caratteristiche positive). In questo post partiremo da due esempi reali (uno banale e l’altro un po’ più complesso) per vedere come il Data Binding può essere di aiuto nello sviluppo delle applicazioni Android.
Binding bidirezionale di un campo di testo
L’esempio classico quando si parla di Data Binding è quello di echo: due campi di testo che sono collegati allo stesso campo del bean per verificare che scrivendo in un campo anche l’altro sia aggiornato in tempo reale. Per scrivere questo esempio iniziamo con una semplice classe Java con un campo di testo:
public class Echo { public String text; }
Questa classe non contiene i getter e i setter per semplificare l’esempio (potremmo parlare per giorni del fatto che su Android in tanti non usano i getter ma non divaghiamo!). Anche nel caso in cui ci siano i getter e i setter non cambierebbe il resto dell’esempio.
Il layout è molto semplice, contiene due EditText
con lo stesso binding nell’attributo text
:
Nell’Activity è necessario impostare il layout usando la classe DataBindingUtil
e l’oggetto su cui effettuare il binding (la classe EchoBinding
viene generata automaticamente in base al layout):
public class EchoActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EchoBinding binding = DataBindingUtil.setContentView(this, R.layout.echo); binding.setEcho(new Echo()); } }
Ok, in un mondo ideale questo esempio scritto così funzionerebbe mettendo i giusti listener sul bean e sulle View del layout e aggiornando il tutto di conseguenza… Nel mondo reale eseguendo questo esempio non vengono aggiunti listener, scrivendo il testo in uno dei due campi di testo l’altro non viene aggiornato automaticamente. L’unico binding che funziona in questo modo è quello iniziale fra la stringa contenuta nella classe Echo
e i due campi di testo. Quando viene richiamato il metodo setEcho
sull’oggetto di binding, nel caso in cui il campo text
contenga un testo tale testo sarà impostato sui due campi nell’interfaccia grafica. Da notare che questa cosa avviene solo dall’oggetto al layout (e non viceversa) e solo quando viene impostato l’oggetto usando setEcho
. Ulteriori modifiche nel valore del campo dell’oggetto non sono riportate nel layout in modo automatico.
Per poter ricevere in modo automatico gli aggiornamenti successivi è necessario definire il campo della classe di tipo ObservableField
e non usare una semplice stringa:
public class Echo { public ObservableFieldtext = new ObservableField<>(); }
La classe ObservableField
(e le altre classi per i tipi nativi come ObservableInt
e ObservableBoolean
) può essere vista come una implementazione del pattern Observable classico (per capirsi non è troppo simile agli Observable di RxJava). In pratica quando viene invocato il metodo setEcho
sull’oggetto di binding viene registrato un listener sull’Observable che viene richiamato ad ogni aggiornamento e che effettuerà il conseguente aggiornamento sulle View contenute nel layout.
Aggiungere un listener per le modifiche all’interfaccia grafica utilizzando il Data Binding non è banale, è necessario comunque passare da un TextWatcher
. L’unico vantaggio che si ha utilizzando il Data Binding è la possibilità di aggiungere il TextWatcher
nel layout:
In questo modo è necessario definire un campo di tipo TextWatcher
all’interno della classe Echo
:
public class Echo { public ObservableFieldtext = new ObservableField<>(); public TextWatcher watcher = new TextWatcherAdapter() { @Override public void afterTextChanged(Editable s) { if (!Objects.equals(text.get(), s.toString())) { text.set(s.toString()); } } }; }
Il controllo all’interno dell’evento è stato aggiunto per evitare cicli infiniti: il layout che aggiorna l’oggetto che a sua volta richiama il listener per aggiornare l’interfaccia grafica.
In questo modo il binding bidirezionale funziona correttamente ma ci sono un paio di cose che non convincono troppo:
- la classe
Echo
dovrebbe essere slegata dall’interfaccia grafica, il dover usare unObservableField
non è proprio pulitissimo e il dover definire qui dentro unTextWatcher
non è il massimo; - per ogni campo su cui eseguire il binding è necessario scrivere molto codice (due campi collegati fra loro).
Definizione di binding custom
Per risolvere problemi appena visti è possibile sfruttare dei binding custom, il framework di Data Binding permette di definirli in modo molto semplice. Per prima cosa definiamo un nostro oggetto simile a ObservableField
ma specifico per le stringhe (cambiamo anche il nome per evitare confusione sia con la classe originale che con gli Observable di RxJava):
public class BindableString extends BaseObservable { private String value; public String get() { return value != null ? value : ""; } public void set(String value) { if (!Objects.equals(this.value, value)) { this.value = value; notifyChange(); } } public boolean isEmpty() { return value == null || value.isEmpty(); } }
Approfittiamo del fatto che stiamo riscrivendo la classe per aggiungere un metodo di utilità isEmpty
ed il controllo per non settare il valore nel caso sia uguale a quello esistente.
Per usare questa classe nell’attributo android:text
è necessario scrivere in una qualunque classe un metodo annotato con BindingConversion
che effettua la conversione:
@BindingConversion public static String convertBindableToString(BindableString bindableString) { return bindableString.get(); }
Per semplificare il codice visto in precedenza è possibile creare un attributo custom usando l’annotation BindingAdapter
. Per esempio possiamo definire un attributo app:binding
in cui impostiamo il valore ed aggiungiamo il TextWatcher
:
@BindingAdapter({"app:binding"}) public static void bindEditText(EditText view, final BindableString bindableString) { Pairpair = (Pair) view.getTag(R.id.bound_observable); if (pair == null || pair.first != bindableString) { if (pair != null) { view.removeTextChangedListener(pair.second); } TextWatcherAdapter watcher = new TextWatcherAdapter() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { bindableString.set(s.toString()); } }; view.setTag(R.id.bound_observable, new Pair<>(bindableString, watcher)); view.addTextChangedListener(watcher); } String newValue = bindableString.get(); if (!view.getText().toString().equals(newValue)) { view.setText(newValue); } }
Un paio di cose da notare:
- questo metodo viene richiamato ogni volta che cambia l’oggetto su cui viene fatto il binding; per evitare di settare più
TextWatcher
per lo stesso campo di testo viene aggiunto (e controllato in seguito) un tag; - prima di impostare il valore viene effettuato un controllo per verificare che il valore sia effettivamente diverso; senza questo controllo il cursore all’interno del campo di testo tornerebbe sempre all’inizio.
A questo punto è molto semplice riscrivere il layout dell’esempio visto in precedenza, basta utilizzare l’attributo app:binding
:
Gestione del cambio di orientation
Un aspetto abbastanza delicato da gestire nelle applicazioni Android è rappresentato dal cambio di orientation del device, anche in questo caso è necessario porre un po’ di attenzione. Infatti l’Activity viene distrutta e, al successivo riavvio, viene creato un nuovo oggetto Echo
e viene rieffettuato il binding. Per questo motivo si riparte da capo perdendo il contenuto della form che l’utente aveva già inserito. 🙁
Il modo giusto per gestire questa cosa consiste nel salvataggio nell’instance state dell’Activity, al momento della creazione l’oggetto Echo
viene ricreato solo nel caso di primo avvio:
public class EchoActivity extends AppCompatActivity { public static final String ECHO = "ECHO"; private Echo echo; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EchoBinding binding = DataBindingUtil.setContentView(this, R.layout.echo); if (savedInstanceState == null) { echo = new Echo(); } else { echo = Parcels.unwrap(savedInstanceState.getParcelable(ECHO)); } binding.setEcho(echo); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(ECHO, Parcels.wrap(echo)); } }
In questo esempio è stato utilizzato il framework Parceler per semplificare la gestione degli oggetti di tipo Parcelable
. Da notare che anche la classe BindableString
deve essere mappata usando Parceler; non è complicato in basta gestire la stringa visto che la lista dei listener può essere ignorata al cambio di orientation.
Una form di login usando il Data Binding
Vediamo un esempio un po’ più complicato, una form di login simile a quella di Amazon:
La validazione parte al primo submit e ad ogni modifica successiva, il campo di testo della password è abilitato solo se viene selezionato il secondo radio button.
Iniziamo proprio dai radio button, il binding in questo caso viene effettuato sul radio group:
Anche in questo caso è stato usato un binding custom, stavolta con un oggetto BindableBoolean
. Il binding con un boolean è un po’ forzato in quanto funziona solo perchè ci sono due radio button, è comunque possibile definire in modo semplice anche altre tipologie di binding (per esempio con un intero). L’implementazione di questa classe e del metodo di definizione del binding è molto simile a quella già vista per le stringhe.
Per quanto riguarda i campi di testo è stato usato un EditText
all’interno di un TextInputLayout
(componente disponibile nella Design Support Library). Nel caso del campo di testo con la password ci sono tre binding da gestire:
- testo: gestito con un binding uguale al precedente esempio;
- errore: anche questo è un binding verso un oggetto di tipo
BindableString
; - enabled: in questo caso il binding è effettuato con lo stesso boolean usato per il
RadioGroup
in modo da avere un aggiornamento automatico ogni volta che l’utente seleziona uno dei due radio button.
Il codice corrispondente a questa parte di layout è il seguente:
Il binding viene effettuato usando una classe LoginInfo
contenente i campi utilizzati all’interno del layout:
public class LoginInfo { public BindableString email = new BindableString(); public BindableString password = new BindableString(); public BindableString emailError = new BindableString(); public BindableString passwordError = new BindableString(); public BindableBoolean existingUser = new BindableBoolean(); public void reset() { email.set(null); password.set(null); emailError.set(null); passwordError.set(null); } }
Il metodo reset
deve essere invocato sul click del corrispondente Button, in questo caso mi sarei aspettato la possibilità di specificare il binding con una sintassi android:onClick="@{loginInfo.reset}"
. In realtà questa cosa non è possibile, per funzionare il metodo reset
dovrebbe ritornare un oggetto OnClickListener
contenente la logica da eseguire. Per adesso non sono riuscito a definire un binding custom per ottenere qualcosa di simile, siamo ancora in una versione beta, vedremo se questa feature sarà aggiunta in una delle prossime versioni.
Possiamo comunque sfruttare l’oggetto generato dal framework per aggiungere il listener:
binding.reset.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { loginInfo.reset(); } });
L’oggetto binding autogenerato contiene un campo per ogni view presente nel layout in cui è definito un id, in questo modo è possibile evitare completamente l’utilizzo di findViewById
! Utilizzando il Data Binding sono meno utili framework come ButterKnife e Holdr (quest’ultimo è già stato deprecato in quanto è un sottoinsieme del Data Binding).
Da notare che internamente il framework non utilizza il metodo findViewById
, infatti nel codice generato è presente un metodo che cerca tutti le View effettuando una unica visita del layout.
In un modo simile è gestita anche il listener per la validazione, viene aggiunto nell’Activity sui campo di email e password:
TextWatcherAdapter watcher = new TextWatcherAdapter() { @Override public void afterTextChanged(Editable s) { loginInfo.validate(getResources()); } }; binding.email.addTextChangedListener(watcher); binding.password.addTextChangedListener(watcher);
All’interno della classe LoginInfo
è stato aggiunto un campo boolean per memorizzare il fatto che l’utente ha già provato a effettuare il login. La validazione avviene controllando tale boolean in modo da non dare errori quando l’utente sta ancora compilando la form:
public boolean loginExecuted; public boolean validate(Resources res) { if (!loginExecuted) { return true; } int emailErrorRes = 0; int passwordErrorRes = 0; if (email.get().isEmpty()) { emailErrorRes = R.string.mandatory_field; } else { if (!Patterns.EMAIL_ADDRESS.matcher(email.get()).matches()) { emailErrorRes = R.string.invalid_email; } } if (existingUser.get() && password.get().isEmpty()) { passwordErrorRes = R.string.mandatory_field; } emailError.set(emailErrorRes != 0 ? res.getString(emailErrorRes) : null); passwordError.set(passwordErrorRes != 0 ? res.getString(passwordErrorRes) : null); return emailErrorRes == 0 && passwordErrorRes == 0; }
Nel listener sul click del Button di login viene impostato il boolean ed eseguita la validazione, in caso positivo viene mostrato un messaggio all’utente usando la classe Snackbar
(disponibile nella design support library):
binding.login.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { loginInfo.loginExecuted = true; if (loginInfo.validate(getResources())) { Snackbar.make( binding.getRoot(), loginInfo.email.get() + " - " + loginInfo.password.get(), Snackbar.LENGTH_LONG ).show(); } } });
Conclusioni
Il framework di Data Binding di Android è ancora in beta, il supporto all’interno di Android Studio è ancora parziale e si vede che ci sono ampi margini di miglioramento. Comunque secondo me è stato progettato e sviluppato molto bene e cambierà (in meglio!) il modo di scrivere le applicazioni Android. La possibilità di poter definire attributi custom è veramente potente, sono molto curioso di vedere come sarà sfruttata dalle varie librerie.
Il sorgente completo degli esempi visti in questo post è disponibile su un progetto GitHub. Se avete esperienze con questo framework e siete fra i temerari (è ancora in beta!) che lo stanno usando su applicazioni vere aspettiamo le vostre impressioni nei commenti!
Pingback: Android 双向 Data Binding | 开发技术前线()