Gestione di una form con il Data Binding Android

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 altro post, 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:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="echo"
            type="it.cosenonjaviste.databinding.echo.Echo"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="16dp">

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Text 1"
            android:text="@{echo.text}"/>

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Text 2"
            android:text="@{echo.text}"/>

    </LinearLayout>
</layout>

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 ObservableField<String> text = 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:

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="Text 1"
    android:text="@{echo.text}"
    android:addTextChangedListener="@{echo.watcher}"/>

In questo modo è necessario definire un campo di tipo TextWatcher all’interno della classe Echo:

public class Echo {
    public ObservableField<String> text = 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 un ObservableField non è proprio pulitissimo e il dover definire qui dentro un TextWatcher 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) {
    Pair<BindableString, TextWatcherAdapter> pair = 
        (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:

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="Text 1"
    app:binding="@{echo.text}"/>

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:

form di login con Data binding

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:

<RadioGroup
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    app:binding="@{loginInfo.existingUser}">

    <RadioButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/new_customer"/>

    <RadioButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/i_have_a_password"/>
</RadioGroup>

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:

<android.support.design.widget.TextInputLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:error="@{loginInfo.passwordError}">

    <EditText
        android:id="@+id/password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:enabled="@{loginInfo.existingUser}"
        android:hint="@string/password"
        android:inputType="textPassword"
        app:binding="@{loginInfo.password}"/>
</android.support.design.widget.TextInputLayout>

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!

Fabio Collini

Software Architect con esperienza su piattaforma J2EE e attualmente focalizzato principalmente in progetti di sviluppo di applicazioni Android. Attualmente sono nel team Android di Cynny, ci stiamo occupando dello sviluppo dell'app Morphcast. Coautore della seconda edizione di Android Programmazione Avanzata e docente di corsi di sviluppo su piattaforma Android. Follow me on Twitter - LinkedIn profile - Google+

  • David Corsalini

    In quali casi è più semplice usare il dataBinding rispetto alla gestione classica? Perchè qui vedo tantissimo codice per fare cose piuttosto semplici.

    • Fabio Collini

      Ciao,
      in effetti il codice in questo post non è poco ma gran parte di questo è generico e può essere riusato su più activity/fragment. In più il data binding è ancora in beta, mi aspetto che nelle prossime release alcune cose siano fattibili in modo più semplice.
      Tornando alla tua domanda per adesso non c’è una risposta, la mia impressione (o forse speranza!) è quella che fra un anno sia lo standard da utilizzare e nessuno si ricordi più di cosa è il metodo findViewById!
      Ciao, Fabio

  • Pingback: Android 双向 Data Binding | 开发技术前线()

  • Pierangelo Dal Maso

    Se posso segnalarti un problema, sto usando le tue classi in un progetto sperimentale.
    Il @BindingAdapter bindEditText, così come lo hai scritto, non funziona nel caso tu lo usi per una form a cui passi successivamente due diversi binding. Ogni volta che fai binding.setLoginInfo() passando un nuovo oggetto loginInfo viene aggiunto un nuovo TextWatcher all’EditText, senza togliere il vecchio, pertanto qualsiasi cosa tu digiti va a modificare sia le proprietà del vecchio che del nuovo oggetto.

    • Fabio Collini

      Ciao,
      grazie della segnalazione, avevo già corretto una cosa simile per i click listener (sul progetto su github è già corretto). In questo caso il problema rimane perché possono essere aggiunti più TextWatcher a un campo di testo 🙁
      Appena ho tempo correggo il codice e il post.

      • Pierangelo Dal Maso

        Grazie, comunque senza i tuoi esempi onestamente non avrei capito molto del funzionamento della faccenda. Abituato a jsf ed angularjs, a tornare a lavorare senza un minimo di binding framework mi stava venendo la depressione 🙁

      • Pierangelo Dal Maso

        Grazie, comunque senza i tuoi esempi onestamente non avrei capito molto del funzionamento della faccenda. Abituato a jsf ed angularjs, a tornare a lavorare senza un minimo di binding framework mi stava venendo la depressione 🙁

        • Fabio Collini

          Ciao, ho corretto il post e l’esempio su github. Ho creato anche una mini libreria con il codice usato nel post per poterlo riusare in modo semplice su più progetti, anche questa è su github: https://github.com/fabioCollini/TwoWayDataBinding Ho usato jitpack, le istruzioni per includerla in un progetto sono qui: https://jitpack.io/#fabioCollini/TwoWayDataBinding/0.3

          Se eri abituato ad Angular passare su Android è parecchio diverso! Però io sono fiducioso sul data binding di Android, anche se ancora non è usatissimo secondo me è veramente fatta bene.

  • Pierangelo Dal Maso

    Che tu sappia, esiste anche qualche framework per la validazione delle form?
    Io poi sto estendendo quello che hai fatto per gestire Date, ed Enum attraverso RadioGroup. Ti interessa se poi ti contribuisco quello che ne esce? (sempre se riesco a farlo funzionare 🙂 )

    • Fabio Collini

      Ciao,
      per la validazione ho sempre provato poco (ma ne ho avuto poco bisogno su Android). Sicuramente non c’è niente che usa il data binding.
      Se vuoi contribuire fai pure, si accettano pull request! 🙂