Maîtriser ViewBinding pour une Gestion de Vues Sûre dans Android

Introduction aux Liaisons de Vues

Dans l'écosystème Android traditionnel, l'accès aux éléments de l'interface utilisateur reposait principalement sur la méthode findViewById(). Bien que fonctionnelle, cette approche présente des risques significatifs : elle est verbeuse, sujette aux erreurs de typage et peut provoquer des exceptions NullPointerException à l'exécution si l'identifiant est incorrect.

Historiquement, plusieurs solutions ont tenté de remédier à ces problèmes. Des bibliothèques comme ButterKnife ont gagné en popularité grâce à l'injection de vues par annotation, mais elles ajoutaient une étape de compilation supplémentaire et sont aujourd'hui dépréciées. D'un autre côté, DataBinding offre une puissance accrue mais introduit une complexité inutile pour de simples liaisons, encourageant parfois l'insertion de logique métier dans les fichiers XML, ce qui nuit à la maintenabilité.

ViewBinding, introduit nativement dans Android Studio, propose un compromis idéal. Il génère des classes de liaison typées statiquement, éliminant les risques d'erreurs d'ID tout en conservant des performances de compilation optimales. De plus, l'IDE facilite la navigation entre le code Java/Kotlin et les layouts XML.

Configuration du Projet

Pour activer cette fonctionnalité, il est nécessaire d'utiliser une version récente d'Android Studio (3.6 ou supérieur). La configuration s'effectue dans le fichier build.gradle du module applicatif en activant la feature correspondante :

android {
    // ... autres configurations
    buildFeatures {
        viewBinding = true
    }
}

Une fois activé, le plugin de build génère automatiquement une classe de liaison pour chaque fichier de layout XML présent dans le projet.

Convention de Nommage

La classe générée suit une convention stricte basée sur le nom du fichier XML. Le nom du fichier est converti en camel case et le suffixe Binding est ajouté. Les underscores sont supprimés. Par exemple, un fichier nommé activity_dashboard.xml produira une classe ActivityDashboardBinding. Cette classe contient une référence pour chaque vue possédant un ID dans le layout.

Implémentation dans les Composants

Activité

Dans une AppCompatActivity, l'inflation se fait via la méthode inflate de la classe générée. La vue racine est ensuite définie comme contenu de l'activité.

public class DashboardActivity extends AppCompatActivity {
    private ActivityDashboardBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityDashboardBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        binding.actionButton.setText("Valider");
        binding.actionButton.setOnClickListener(v -> {
            // Gestion du clic
        });
    }
}

Fragment

Pour les fragments, la liaison doit être gérée cuidadosément pour éviter les fuites de mémoire. Il est recommandé de nullifier la référence dans onDestroyView.

public class ProfileFragment extends Fragment {
    private FragmentProfileBinding binding;

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        binding = FragmentProfileBinding.inflate(inflater, container, false);
        return binding.getRoot();
    }

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        binding.userNameText.setText("Utilisateur");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }
}

Dialogues Personnalisés

L'utilisation dans les dialogues suit la même logique d'inflation pour définir le contenu de la fenêtre.

public class InfoDialog extends Dialog {
    private DialogInfoBinding binding;

    public InfoDialog(@NonNull Context context) {
        super(context);
        binding = DialogInfoBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        binding.messageText.setText("Information importante");
    }
}

Adaptateurs RecyclerView

ViewBinding simplifie considérablement la gestion des ViewHolders. La liaison peut être stockée directement dans le ViewHolder.

public class ProductListAdapter extends RecyclerView.Adapter<ProductListAdapter.ProductHolder> {

    @NonNull
    @Override
    public ProductHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        ItemProductBinding itemBinding = ItemProductBinding.inflate(inflater, parent, false);
        return new ProductHolder(itemBinding);
    }

    @Override
    public void onBindViewHolder(@NonNull ProductHolder holder, int position) {
        holder.binding.productName.setText("Produit " + position);
    }

    @Override
    public int getItemCount() {
        return 10;
    }

    static class ProductHolder extends RecyclerView.ViewHolder {
        ItemProductBinding binding;

        public ProductHolder(@NonNull ItemProductBinding itemBinding) {
            super(itemBinding.getRoot());
            this.binding = itemBinding;
        }
    }
}

Exemple Kotlin

En Kotlin, la syntaxe est encore plus concise grâce à la délégation de propriétés ou à la gestion directe dans le constructeur du ViewHolder.

class NotificationAdapter : RecyclerView.Adapter<NotificationAdapter.NotifHolder>() {

    class NotifHolder(val binding: MessageAlertBinding) : RecyclerView.ViewHolder(binding.root)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotifHolder {
        val bind = MessageAlertBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return NotifHolder(bind)
    }
    
    // ... onBindViewHolder
}

Vues Personnalisées

Pour les vues custom héritant de ViewGroup, l'inflation peut se faire directement dans le bloc d'initialisaiton.

class PinCodeInput(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) {
    private var binding: LayoutPinCodeBinding

    init {
        binding = LayoutPinCodeBinding.inflate(LayoutInflater.from(context), this, true)
    }
}

Gestion des Balises Include

ViewBinding prend en charge les balises <include>. Si un layout inclus possède un ID, une sous-classe de liaison est générée dans la classe principale pour accéder à ses vues.

Exemple de layout principal :

<androidx.constraintlayout.widget.ConstraintLayout ... >
    <include
        android:id="@+id/header_section"
        layout="@layout/layout_header"
        ... />
    <Button android:id="@+id/submit_btn" ... />
</androidx.constraintlayout.widget.ConstraintLayout>

Accès dans l'activité :

binding.headerSection.headerTitle.setText("Titre Inclus");
binding.headerSection.headerTitle.setOnClickListener(v -> {
    // Action
});

Si nécessaire, il est possible de binder le layout inclus séparément en utilisant la méthode bind() sur la vue racine.

Exclusion de Layouts

Il est possible d'empêcher la génération de classe de liaison pour un fichier XML spécifique. Cela se fait en ajoutant l'attribut tools:viewBindingIgnore="true" sur la vue racine du layout.

<LinearLayout 
    xmlns:android="..."
    xmlns:tools="..."
    tools:viewBindingIgnore="true">
    ...
</LinearLayout>

Encapsulation Générique

Pour réduire la boilerplate dans les activités, on peut créer une classe de base générique utilisant la réflexion pour inflater la liaison automatiquement.

public abstract class BaseBoundActivity<T extends ViewBinding> extends AppCompatActivity {
    protected T binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        try {
            Type type = getClass().getGenericSuperclass();
            Class<?> clazz = (Class<?>) ((ParameterizedType) type).getActualTypeArguments()[0];
            Method inflateMethod = clazz.getDeclaredMethod("inflate", LayoutInflater.class);
            binding = (T) inflateMethod.invoke(null, getLayoutInflater());
            setContentView(binding.getRoot());
        } catch (Exception e) {
            e.printStackTrace();
        }
        initViews();
    }

    protected abstract void initViews();
}

Étiquettes: android-viewbinding android-studio Java-Android kotlin-android Android-Gradle

Publié le 28 octobre à 20h11