DAM · M0489 · Introducción a Android

Controles en Android

Resumen completo del tema Controls: cada control con su XML, su código (Java y Kotlin — cambia arriba a la derecha), un esquema visual de cómo funciona y las correcciones de lo que estaba mal escrito o se ha quedado antiguo.

Button · ToggleButton · ImageButtonImageView · TextView · EditTextCheckBox · RadioButtonAdapter · Spinner · ListView · GridView

00 Cómo funciona un control

Todos los controles siguen el mismo ciclo. Si entiendes esto, entiendes el tema entero.

1 · XMLDeclaras el control en res/layout/activity_main.xml con un android:id.
→
2 · R.idAndroid genera una constante R.id.miBoton para ese id.
→
3 · ActivityEn onCreate(), tras setContentView(), obtienes la referencia con findViewById().
→
4 · ListenerLe asignas un listener (setOnClickListener, setOnCheckedChangeListener…).
→
5 · EventoEl usuario interactúa → Android llama a tu método → lees/cambias el estado.
Java · MainActivity.java (plantilla base)
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);   // 1. carga el XML

        Button btn = findViewById(R.id.btnAceptar); // 2-3. referencia (sin cast desde API 26)
        btn.setOnClickListener(v -> {               // 4. listener (lambda)
            Toast.makeText(this, "Pulsado", Toast.LENGTH_SHORT).show(); // 5. acción
        });
    }
}
Kotlin · MainActivity.kt (plantilla base)
class MainActivity : AppCompatActivity() {

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

        val btn = findViewById<Button>(R.id.btnAceptar)
        btn.setOnClickListener {
            Toast.makeText(this, "Pulsado", Toast.LENGTH_SHORT).show()
        }
    }
}
Actualizado respecto a los apuntes
  • Ya no hace falta el cast (Button) findViewById(...): desde API 26 el método es genérico.
  • Las clases anónimas new View.OnClickListener() { ... } siguen funcionando, pero hoy se escriben como lambda (v -> { ... }), que es más corto. En los ejemplos pongo la lambda y, donde ayuda, también la versión larga.
  • En Kotlin, los synthetics (kotlinx.android.synthetic) están eliminados. Se usa findViewById o ViewBinding.

Bonus: ViewBinding (la forma recomendada hoy)

Evita los findViewById uno a uno y los errores de id/tipo. Se activa en build.gradle del módulo:

Gradle · app/build.gradle.kts
android {
    buildFeatures {
        viewBinding = true
    }
}
Java · con ViewBinding
private ActivityMainBinding binding;   // clase generada a partir de activity_main.xml

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

    binding.btnAceptar.setOnClickListener(v -> binding.lblMensaje.setText("Hola"));
}
Kotlin · con ViewBinding
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    binding.btnAceptar.setOnClickListener { binding.lblMensaje.text = "Hola" }
}

En el resto del resumen uso findViewById porque es lo que sale en los apuntes y en el examen; con ViewBinding la lógica es idéntica.

01 Button

El botón básico: muestra un texto y reacciona al click.

12:00▮▮▮
Demo Button
clicks: 0
PropiedadPara qué
android:textTexto del botón (mejor @string/...)
app:backgroundTintColor de fondo (ver aviso ↓)
android:textColorColor del texto
android:textSizeTamaño, siempre en sp
android:fontFamily / android:textStyleTipo de letra / negrita, cursiva
XML · activity_main.xml
<Button
    android:id="@+id/btnBoton1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/pulsar"
    android:textColor="@android:color/white"
    android:textSize="18sp"
    android:textStyle="bold"
    android:fontFamily="sans-serif-medium"
    app:backgroundTint="@color/verde" />
Corregido en los apuntes
  • android:textcolor → es android:textColor (con C mayúscula; los atributos XML distinguen mayúsculas).
  • android:background para el color: en proyectos nuevos el tema es Material y el <Button> se convierte en MaterialButton, que ignora android:background. Usa app:backgroundTint (o pon app:backgroundTint="@null" si quieres un drawable de fondo).
  • android:typeface está prácticamente en desuso: usa android:fontFamily (fuente) y android:textStyle (bold/italic).
  • No escribas el texto a pelo ("Pulsar"): Android Studio da warning. Va en res/values/strings.xml.

02 ToggleButton

Botón con dos estados (pulsado / no pulsado) que se queda en el estado elegido. Cada estado tiene su propio texto.

12:00▮▮▮
Demo ToggleButton
isChecked() = false

Cómo funciona: internamente es un CompoundButton (igual que CheckBox, RadioButton y Switch). Guarda un booleano checked.

  • android:textOn → texto cuando está activado
  • android:textOff → texto cuando está desactivado
  • isChecked() / setChecked(bool) → leer / cambiar el estado
XML
<ToggleButton
    android:id="@+id/tgbBoton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textOn="@string/on"
    android:textOff="@string/off" />
Corregido En los apuntes pone android:textoOff — es android:textOff. Con la “o” extra el atributo no existe y el XML no compila.
Alternativa moderna Para on/off hoy se usa más un interruptor: com.google.android.material.materialswitch.MaterialSwitch (o SwitchCompat). Se programa exactamente igual (isChecked, setOnCheckedChangeListener).

03 ImageButton

Botón que muestra una imagen en lugar de texto. La imagen se indica con android:src y suele estar en res/drawable.

12:00▮▮▮
Demo ImageButton
esperando click…

res/drawable/ok.png (o mejor ok.xml vectorial) → se referencia como @drawable/ok en XML y R.drawable.ok en código.

El nombre del fichero: solo minúsculas, números y _.

XML
<ImageButton
    android:id="@+id/btnImagen"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/ok"
    android:contentDescription="@string/aceptar" />
Añadido
  • android:contentDescription es obligatorio en la práctica (accesibilidad: lo lee TalkBack). Sin él Android Studio marca warning.
  • En vez de PNG, usa iconos vectoriales: botón derecho en res → New → Vector Asset. Se ven nítidos en todas las pantallas.

04 Eventos de un botón (onClick)

Hay dos formas de reaccionar al click. La segunda es la buena.

Forma A · atributo XML

Pones android:onClick="metodo" y en la Activity creas public void metodo(View v).

Desaconsejado Se busca el método por reflexión: si cambias el nombre no da error al compilar, sino que la app peta al pulsar. Además no funciona en Fragments.

Forma B · listener en código ✅

Obtienes el botón y le asignas un View.OnClickListener con setOnClickListener(). Comprobado al compilar y funciona en todas partes.

Forma A (para reconocerla en el examen)

XML
<Button
    android:id="@+id/btnBoton1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/pulsar"
    android:onClick="pulsarBoton" />
Java
// Debe ser public, void y recibir un View. Si no, crash al pulsar.
public void pulsarBoton(View view) {
    TextView lbl = findViewById(R.id.lblMensaje);
    lbl.setText("Botón pulsado");
}
Kotlin
fun pulsarBoton(view: View) {
    findViewById<TextView>(R.id.lblMensaje).text = "Botón pulsado"
}

Forma B (la recomendada)

Java · versión clásica y versión lambda
Button btnBoton1 = findViewById(R.id.btnBoton1);
TextView lblMensaje = findViewById(R.id.lblMensaje);

// Versión clásica (clase anónima) — la de los apuntes
btnBoton1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        lblMensaje.setText("Botón 1 pulsado");
    }
});

// Versión moderna (lambda) — hace exactamente lo mismo
btnBoton1.setOnClickListener(v -> lblMensaje.setText("Botón 1 pulsado"));
Kotlin
val btnBoton1 = findViewById<Button>(R.id.btnBoton1)
val lblMensaje = findViewById<TextView>(R.id.lblMensaje)

btnBoton1.setOnClickListener {
    lblMensaje.text = "Botón 1 pulsado"
}

Saber el estado de un ToggleButton

Java
ToggleButton tgbBoton = findViewById(R.id.tgbBoton);

// Mejor escuchar el CAMBIO de estado que el click:
tgbBoton.setOnCheckedChangeListener((buttonView, isChecked) -> {
    if (isChecked) {
        lblMensaje.setText("Botón ON");
    } else {
        lblMensaje.setText("Botón OFF");
    }
});

// Lo de los apuntes (onClick + isChecked()) también funciona:
tgbBoton.setOnClickListener(v -> {
    lblMensaje.setText(tgbBoton.isChecked() ? "Botón ON" : "Botón OFF");
});
Kotlin
val tgbBoton = findViewById<ToggleButton>(R.id.tgbBoton)

tgbBoton.setOnCheckedChangeListener { _, isChecked ->
    lblMensaje.text = if (isChecked) "Botón ON" else "Botón OFF"
}
¿Por qué mejor setOnCheckedChangeListener? Porque también salta si el estado cambia por código (setChecked(true)), no solo por un toque, y ya te da el booleano isChecked como parámetro.

05 Personalizar un botón: selector

Poner una imagen fija en el fondo no da sensación de “botón”. Un selector es un XML en res/drawable que elige un fondo distinto según el estado del control.

12:00▮▮▮
Selector
Por defectoCon toggle_style.xml

Cómo lo evalúa Android: recorre los <item> de arriba abajo y usa el primero que cumple. Por eso el item sin condición (el “por defecto”) va siempre el último.

EstadoCuándo
state_checkedToggleButton/CheckBox activado
state_pressedMientras se mantiene pulsado
state_enabled="false"Control deshabilitado
state_focusedTiene el foco
XML · res/drawable/toggle_style.xml (versión apuntes, con PNG)
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="false" android:drawable="@drawable/toggle_off" />
    <item android:state_checked="true"  android:drawable="@drawable/toggle_on" />
</selector>
XML · res/drawable/toggle_style.xml (versión mejorada, sin imágenes)
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 1º estado más específico -->
    <item android:state_checked="true">
        <shape android:shape="rectangle">
            <solid android:color="#2E9E5B" />
            <corners android:radius="24dp" />
        </shape>
    </item>
    <!-- último: estado por defecto -->
    <item>
        <shape android:shape="rectangle">
            <solid android:color="#CFD8DC" />
            <corners android:radius="24dp" />
        </shape>
    </item>
</selector>
XML · usarlo en el botón
<ToggleButton
    android:id="@+id/tgbBoton2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textOn="@string/on"
    android:textOff="@string/off"
    android:padding="12dp"
    android:background="@drawable/toggle_style" />
Corregido / actualizado
  • En los apuntes pone android: background (con espacio) — se escribe android:background todo junto.
  • La carpeta es res/drawable (no “recursos/res/drawable”).
  • Con ToggleButton/CheckBox el android:background funciona. Si lo aplicas a un <Button> normal con tema Material, añade también app:backgroundTint="@null" o no se verá.
  • Con <shape> no necesitas diseñar PNG: se adapta a cualquier tamaño y densidad.

06 ImageView

Muestra una imagen. No es clicable por defecto (para eso está ImageButton).

XML
<ImageView
    android:id="@+id/imgFoto"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/unaimatge"
    android:adjustViewBounds="true"
    android:maxWidth="200dp"
    android:maxHeight="200dp"
    android:scaleType="fitCenter"
    android:contentDescription="@string/desc_foto" />
Java
ImageView imgFoto = findViewById(R.id.imgFoto);
imgFoto.setImageResource(R.drawable.otra_imagen);
Kotlin
val imgFoto = findViewById<ImageView>(R.id.imgFoto)
imgFoto.setImageResource(R.drawable.otra_imagen)
Corregido / añadido
  • El método es setImageResource(...) — en los apuntes pone setImageResorce (falta la u).
  • maxWidth y maxHeight no hacen nada si no pones android:adjustViewBounds="true". Esto no lo dicen los apuntes y es la causa típica de “no me funciona”.
  • scaleType controla cómo encaja la imagen: fitCenter (entera, centrada), centerCrop (llena y recorta), fitXY (deforma).
  • Para imágenes de internet no se usa setImageResource: se usa una librería como Coil o Glide.

07 TextView

Etiqueta de texto de solo lectura.

Propiedad XMLMétodo en códigoQué hace
android:textsetText() / getText()Texto mostrado
android:backgroundsetBackgroundColor()Color de fondo
android:textColorsetTextColor()Color del texto
android:textSizesetTextSize()Tamaño (en sp)
android:textStyle / fontFamilysetTypeface()Negrita, cursiva, fuente
XML
<TextView
    android:id="@+id/lblEtiqueta"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/valor"
    android:background="#AA44FF"
    android:textColor="@android:color/white"
    android:textSize="20sp"
    android:textStyle="bold|italic" />
Java · leer, concatenar, escribir y cambiar color
TextView lblEtiqueta = findViewById(R.id.lblEtiqueta);

String texto = lblEtiqueta.getText().toString(); // getText() devuelve CharSequence
texto += "123";
lblEtiqueta.setText(texto);

// Color desde resources (forma correcta) o constante
lblEtiqueta.setBackgroundColor(ContextCompat.getColor(this, R.color.azul));
lblEtiqueta.setBackgroundColor(Color.BLUE);
Kotlin
val lblEtiqueta = findViewById<TextView>(R.id.lblEtiqueta)

val texto = lblEtiqueta.text.toString() + "123"
lblEtiqueta.text = texto

lblEtiqueta.setBackgroundColor(ContextCompat.getColor(this, R.color.azul))
Actualizado
  • getResources().getColor(id) está deprecated → usa ContextCompat.getColor(context, id).
  • Al pasar un número a setText() ojo: setText(5) busca un recurso con id 5 y peta (Resources$NotFoundException). Usa setText(String.valueOf(5)).

08 EditText

Caja para que el usuario escriba. Es una subclase de TextView, así que hereda todas sus propiedades.

12:00▮▮▮
Demo EditText
getText().toString() = ""

Clave del examen: getText() en un EditText devuelve un objeto Editable, no un String. Por eso siempre se hace .toString().

android:hint = texto gris de ayuda que desaparece al escribir.
android:inputType = tipo de teclado (text, number, textEmailAddress, textPassword, phone…).

XML
<EditText
    android:id="@+id/txtNombre"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="@string/hint_nombre"
    android:inputType="textPersonName"
    android:autofillHints="name" />
Java
EditText txtNombre = findViewById(R.id.txtNombre);
Button btnSaludar = findViewById(R.id.btnSaludar);
TextView lblSaludo = findViewById(R.id.lblSaludo);

btnSaludar.setOnClickListener(v -> {
    String nombre = txtNombre.getText().toString().trim(); // Editable -> String
    if (nombre.isEmpty()) {
        txtNombre.setError("Escribe un nombre");   // muestra el error en la caja
        return;
    }
    lblSaludo.setText("Hola, " + nombre);
    txtNombre.setText("");                          // limpiar
});
Kotlin
val txtNombre = findViewById<EditText>(R.id.txtNombre)
val btnSaludar = findViewById<Button>(R.id.btnSaludar)
val lblSaludo = findViewById<TextView>(R.id.lblSaludo)

btnSaludar.setOnClickListener {
    val nombre = txtNombre.text.toString().trim()
    if (nombre.isEmpty()) {
        txtNombre.error = "Escribe un nombre"
        return@setOnClickListener
    }
    lblSaludo.text = "Hola, $nombre"
    txtNombre.setText("")      // en Kotlin .text = "" no compila (es Editable)
}
Versión Material En apps modernas el EditText se envuelve en TextInputLayout + TextInputEditText (etiqueta flotante, contador, errores bonitos). El código para leer el texto es el mismo.

09 CheckBox

Marcar / desmarcar opciones independientes (puedes marcar varias).

12:00▮▮▮
Demo CheckBox
✓ Acepto las condiciones
✓ Recibir novedades
onCheckedChanged…
  • isChecked() → ¿está marcado?
  • setChecked(true/false) → marcarlo por código
  • toggle() → invertir el estado
  • Evento: setOnCheckedChangeListener → método onCheckedChanged(CompoundButton, boolean)
XML
<CheckBox
    android:id="@+id/chkMarcame"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/marcame"
    android:checked="false" />
Java
CheckBox chkMarcame = findViewById(R.id.chkMarcame);

// Leer y cambiar el estado
if (chkMarcame.isChecked()) {
    chkMarcame.setChecked(false);
}

// Escuchar cambios (versión clásica)
chkMarcame.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if (isChecked) {
            chkMarcame.setText("Checkbox marcado!");
        } else {
            chkMarcame.setText("Checkbox desmarcado!");
        }
    }
});

// Igual con lambda
chkMarcame.setOnCheckedChangeListener((buttonView, isChecked) ->
        chkMarcame.setText(isChecked ? "Checkbox marcado!" : "Checkbox desmarcado!"));
Kotlin
val chkMarcame = findViewById<CheckBox>(R.id.chkMarcame)

if (chkMarcame.isChecked) chkMarcame.isChecked = false

chkMarcame.setOnCheckedChangeListener { _, isChecked ->
    chkMarcame.text = if (isChecked) "Checkbox marcado!" else "Checkbox desmarcado!"
}
Ojo con el import Es CompoundButton.OnCheckedChangeListener. Existe otro RadioGroup.OnCheckedChangeListener con el mismo nombre y parámetros distintos; si Android Studio importa el que no toca, no compila.

10 RadioButton y RadioGroup

Grupo de opciones donde solo una puede estar marcada. Los RadioButton se meten dentro de un RadioGroup, que es quien gestiona la exclusividad y el evento.

12:00▮▮▮
Demo RadioGroup
Opción 1
Opción 2
getCheckedRadioButtonId() = -1
Método de RadioGroupQué hace
check(id)Marca la opción con ese id
clearCheck()Desmarca todas
getCheckedRadioButtonId()Id de la marcada, o -1 si ninguna

El listener se pone en el RadioGroup, no en cada RadioButton.

XML
<RadioGroup
    android:id="@+id/grbGrupo1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <RadioButton
        android:id="@+id/radio1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/opcion_1" />

    <RadioButton
        android:id="@+id/radio2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/opcion_2" />
</RadioGroup>
Java
RadioGroup grbGrupo1 = findViewById(R.id.grbGrupo1);
TextView lblMensaje = findViewById(R.id.lblMensaje);

grbGrupo1.clearCheck();                 // ninguna marcada
grbGrupo1.check(R.id.radio1);           // marca la opción 1
int idSeleccionado = grbGrupo1.getCheckedRadioButtonId(); // -1 si no hay ninguna

// Evento: cambio de opción
grbGrupo1.setOnCheckedChangeListener((group, checkedId) -> {
    // ⚠ NO usar switch(checkedId) { case R.id.radio1: ... } (ver aviso)
    if (checkedId == R.id.radio1) {
        lblMensaje.setText("Opción 1");
    } else if (checkedId == R.id.radio2) {
        lblMensaje.setText("Opción 2");
    }
});

// Alternativa: obtener el RadioButton y su texto
RadioButton rb = findViewById(grbGrupo1.getCheckedRadioButtonId());
if (rb != null) lblMensaje.setText(rb.getText());
Kotlin
val grbGrupo1 = findViewById<RadioGroup>(R.id.grbGrupo1)
val lblMensaje = findViewById<TextView>(R.id.lblMensaje)

grbGrupo1.clearCheck()
grbGrupo1.check(R.id.radio1)
val idSeleccionado = grbGrupo1.checkedRadioButtonId

grbGrupo1.setOnCheckedChangeListener { _, checkedId ->
    lblMensaje.text = when (checkedId) {   // en Kotlin 'when' sí funciona
        R.id.radio1 -> "Opción 1"
        R.id.radio2 -> "Opción 2"
        else -> ""
    }
}
Importante (cosa que ha cambiado)
  • Desde Android Gradle Plugin 8 los R.id.* ya no son constantes (final). En Java un switch con case R.id.radio1: da el error “Constant expression required”. Usa if / else if. Muchos ejemplos de internet y apuntes antiguos usan switch.
  • El evento se llama onCheckedChanged (con d); en los apuntes aparece como “onCheckedChange”.

11 Adapters

Spinner, ListView y GridView no saben de dónde vienen los datos. Se los da un adaptador, que hace de puente y además crea la vista de cada elemento.

DatosString[], ArrayList, Cursor de BD…
→
AdaptergetCount() cuántos hay
getItem(pos) cuál es
getView(pos) crea la fila con un layout
→
ControlSpinner · ListView · GridView
(setAdapter())
AdaptadorDatos de origenEstado hoy
ArrayAdapterArray o List de objetos (usa su toString())✅ Se usa (sobre todo con Spinner)
SimpleAdapterList de Map → vistas de un layout XML⚠ Antiguo, poco usado
SimpleCursorAdapterColumnas de un Cursor (SQLite)✕ Legacy → hoy Room + RecyclerView
Java · crear un ArrayAdapter
String[] datos = {"Elem1", "Elem2", "Elem3", "Elem4", "Elem5"};

ArrayAdapter<String> adaptador = new ArrayAdapter<>(
        this,                                   // 1. contexto (la Activity)
        android.R.layout.simple_spinner_item,   // 2. layout de cada elemento (un TextView)
        datos);                                 // 3. los datos
Kotlin · crear un ArrayAdapter
val datos = arrayOf("Elem1", "Elem2", "Elem3", "Elem4", "Elem5")

val adaptador = ArrayAdapter(
    this,                                   // contexto
    android.R.layout.simple_spinner_item,   // layout de cada elemento
    datos                                   // datos
)
Los 3 parámetros (pregunta típica) contexto (la Activity), id del layout para cada elemento (los de android.R.layout.* son predefinidos y contienen un solo TextView) y el array/lista de datos.

12 Spinner (lista desplegable)

Muestra el elemento seleccionado; al tocarlo se abre una lista emergente con todas las opciones.

12:00▮▮▮
Demo Spinner
Elem1▾
Elem1
Elem2
Elem3
Elem4
Elem5
onItemSelected(position = 0)

Dos layouts distintos:

  • simple_spinner_item (en el constructor) → cómo se ve el elemento cerrado.
  • simple_spinner_dropdown_item (con setDropDownViewResource()) → cómo se ven las filas de la lista abierta.

⚠ onItemSelected se dispara también al arrancar con la posición 0.

XML
<Spinner
    android:id="@+id/cmbOpciones"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />
Java
Spinner cmbOpciones = findViewById(R.id.cmbOpciones);
TextView lblMensaje = findViewById(R.id.lblMensaje);

String[] datos = {"Elem1", "Elem2", "Elem3", "Elem4", "Elem5"};
ArrayAdapter<String> adaptador = new ArrayAdapter<>(this,
        android.R.layout.simple_spinner_item, datos);
adaptador.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
cmbOpciones.setAdapter(adaptador);

// Evento: OnItemSelectedListener tiene DOS métodos obligatorios (no admite lambda)
cmbOpciones.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        lblMensaje.setText("Seleccionado: " + parent.getItemAtPosition(position));
    }

    @Override
    public void onNothingSelected(AdapterView<?> parent) {
        lblMensaje.setText("");
    }
});
Kotlin
val cmbOpciones = findViewById<Spinner>(R.id.cmbOpciones)
val lblMensaje = findViewById<TextView>(R.id.lblMensaje)

val datos = arrayOf("Elem1", "Elem2", "Elem3", "Elem4", "Elem5")
val adaptador = ArrayAdapter(this, android.R.layout.simple_spinner_item, datos)
adaptador.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
cmbOpciones.adapter = adaptador

cmbOpciones.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
    override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
        lblMensaje.text = "Seleccionado: ${parent.getItemAtPosition(position)}"
    }
    override fun onNothingSelected(parent: AdapterView<*>) {
        lblMensaje.text = ""
    }
}

Extra: datos desde strings.xml (mejor práctica)

XML · res/values/strings.xml
<string-array name="opciones">
    <item>Elem1</item>
    <item>Elem2</item>
    <item>Elem3</item>
</string-array>
Java
ArrayAdapter<CharSequence> adaptador = ArrayAdapter.createFromResource(this,
        R.array.opciones, android.R.layout.simple_spinner_item);
adaptador.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
cmbOpciones.setAdapter(adaptador);
// O directamente en el XML del Spinner: android:entries="@array/opciones"
Kotlin
val adaptador = ArrayAdapter.createFromResource(
    this, R.array.opciones, android.R.layout.simple_spinner_item
)
adaptador.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
cmbOpciones.adapter = adaptador

13 ListView

Lista de opciones visible directamente (sin desplegar). Si no caben, hace scroll sola.

12:00▮▮▮
Demo ListView
Elem1
Elem2
Elem3
Elem4
onItemClick…

onItemClick(parent, view, position, id)

  • parent → el ListView
  • view → la fila pulsada
  • position → 3er parámetro: índice en el array (empieza en 0)
  • id → id de la fila (en ArrayAdapter = position)
XML
<ListView
    android:id="@+id/lstOpciones"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
Java
ListView lstOpciones = findViewById(R.id.lstOpciones);
TextView lblMensaje = findViewById(R.id.lblMensaje);

String[] datos = {"Elem1", "Elem2", "Elem3", "Elem4", "Elem5"};
ArrayAdapter<String> adaptador = new ArrayAdapter<>(this,
        android.R.layout.simple_list_item_1, datos);
lstOpciones.setAdapter(adaptador);

// Evento click en un elemento (OnItemClickListener tiene 1 método → admite lambda)
lstOpciones.setOnItemClickListener((parent, view, position, id) ->
        lblMensaje.setText("Opción: " + datos[position]));

/* Versión clásica:
lstOpciones.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        lblMensaje.setText("Opción: " + parent.getItemAtPosition(position));
    }
});
*/
Kotlin
val lstOpciones = findViewById<ListView>(R.id.lstOpciones)
val lblMensaje = findViewById<TextView>(R.id.lblMensaje)

val datos = arrayOf("Elem1", "Elem2", "Elem3", "Elem4", "Elem5")
lstOpciones.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, datos)

lstOpciones.setOnItemClickListener { _, _, position, _ ->
    lblMensaje.text = "Opción: ${datos[position]}"
}
Corregido / antiguo
  • El método es setOnItemClickListener — en los apuntes pone setOnItemClicListener (falta la k).
  • Si los datos cambian (p.ej. una ArrayList), tras modificar la lista llama a adaptador.notifyDataSetChanged() para refrescar. Con un String[] fijo no puedes añadir elementos.
  • ListView está considerado legacy: funciona, pero en apps nuevas se usa RecyclerView (ver sección 15).

14 GridView

Como ListView pero en forma de tabla: filas y columnas. Los datos se rellenan por filas.

12:00▮▮▮
Demo GridView
numColumns = 3
PropiedadQué hace
numColumnsNº de columnas, o auto_fit (lo calcula el sistema)
columnWidthAncho de cada columna
horizontalSpacingEspacio horizontal entre celdas
verticalSpacingEspacio vertical entre celdas
stretchModeQué hacer con el espacio sobrante: columnWidth (se lo quedan las columnas) o spacingWidth (se lo quedan los huecos)
XML
<GridView
    android:id="@+id/grdOpciones"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:numColumns="auto_fit"
    android:columnWidth="80dp"
    android:horizontalSpacing="5dp"
    android:verticalSpacing="10dp"
    android:stretchMode="columnWidth" />
Java
GridView grdOpciones = findViewById(R.id.grdOpciones);
TextView lblMensaje = findViewById(R.id.lblMensaje);

String[] datos = new String[25];
for (int i = 0; i < datos.length; i++) {
    datos[i] = "Dato" + (i + 1);
}

ArrayAdapter<String> adaptador = new ArrayAdapter<>(this,
        android.R.layout.simple_list_item_1, datos);
grdOpciones.setAdapter(adaptador);

grdOpciones.setOnItemClickListener((parent, view, position, id) ->
        lblMensaje.setText("Pulsado: " + datos[position]));
Kotlin
val grdOpciones = findViewById<GridView>(R.id.grdOpciones)
val lblMensaje = findViewById<TextView>(R.id.lblMensaje)

val datos = Array(25) { i -> "Dato${i + 1}" }
grdOpciones.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, datos)

grdOpciones.setOnItemClickListener { _, _, position, _ ->
    lblMensaje.text = "Pulsado: ${datos[position]}"
}
Antiguo GridView también es legacy. Hoy: RecyclerView con GridLayoutManager(this, 3).

15 Lo moderno: RecyclerView

No está en los apuntes, pero es lo que sustituye a ListView y GridView. Es más eficiente porque recicla las filas que salen de pantalla (patrón ViewHolder obligatorio).

List<String>tus datos
→
Adapter + ViewHolderonCreateViewHolder infla la fila
onBindViewHolder pone los datos
getItemCount cuántas
→
RecyclerView+ LayoutManager
Linear (lista) · Grid (tabla)
Java · MiAdapter.java
public class MiAdapter extends RecyclerView.Adapter<MiAdapter.VH> {

    public interface OnItemClick { void onClick(int position); }

    private final List<String> datos;
    private final OnItemClick listener;

    public MiAdapter(List<String> datos, OnItemClick listener) {
        this.datos = datos;
        this.listener = listener;
    }

    static class VH extends RecyclerView.ViewHolder {
        TextView txt;
        VH(View v) {
            super(v);
            txt = v.findViewById(android.R.id.text1);
        }
    }

    @NonNull @Override
    public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(android.R.layout.simple_list_item_1, parent, false);
        return new VH(v);
    }

    @Override
    public void onBindViewHolder(@NonNull VH holder, int position) {
        holder.txt.setText(datos.get(position));
        holder.itemView.setOnClickListener(v ->
                listener.onClick(holder.getBindingAdapterPosition()));
    }

    @Override
    public int getItemCount() { return datos.size(); }
}

// En la Activity:
RecyclerView rv = findViewById(R.id.rvOpciones);
List<String> datos = Arrays.asList("Elem1", "Elem2", "Elem3");
rv.setLayoutManager(new LinearLayoutManager(this));      // lista (≈ ListView)
// rv.setLayoutManager(new GridLayoutManager(this, 3));  // tabla (≈ GridView)
rv.setAdapter(new MiAdapter(datos, pos ->
        Toast.makeText(this, datos.get(pos), Toast.LENGTH_SHORT).show()));
Kotlin · MiAdapter.kt
class MiAdapter(
    private val datos: List<String>,
    private val onClick: (Int) -> Unit
) : RecyclerView.Adapter<MiAdapter.VH>() {

    class VH(v: View) : RecyclerView.ViewHolder(v) {
        val txt: TextView = v.findViewById(android.R.id.text1)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
        val v = LayoutInflater.from(parent.context)
            .inflate(android.R.layout.simple_list_item_1, parent, false)
        return VH(v)
    }

    override fun onBindViewHolder(holder: VH, position: Int) {
        holder.txt.text = datos[position]
        holder.itemView.setOnClickListener { onClick(holder.bindingAdapterPosition) }
    }

    override fun getItemCount() = datos.size
}

// En la Activity:
val rv = findViewById<RecyclerView>(R.id.rvOpciones)
val datos = listOf("Elem1", "Elem2", "Elem3")
rv.layoutManager = LinearLayoutManager(this)       // o GridLayoutManager(this, 3)
rv.adapter = MiAdapter(datos) { pos ->
    Toast.makeText(this, datos[pos], Toast.LENGTH_SHORT).show()
}

16 Errores y cosas antiguas corregidas

Todo lo que he cambiado respecto a los apuntes, en una tabla.

En los apuntesCorrecto / actualTipo
android:textcolorandroid:textColorErrata
android:textoOffandroid:textOffErrata
android: backgroundandroid:background (sin espacio)Errata
setImageResorce(...)setImageResource(...)Errata
setOnItemClicListenersetOnItemClickListenerErrata
onCheckedChangeonCheckedChangedErrata
“recursos/res/drawable”res/drawableErrata
(Button) findViewById(...)Sin cast (API 26+) o ViewBindingAntiguo
new View.OnClickListener(){...}Lambda v -> {...}Antiguo (funciona)
android:onClick en XMLsetOnClickListener en códigoDesaconsejado
android:background en Buttonapp:backgroundTint (tema Material)Cambio de comportamiento
android:typefaceandroid:fontFamily + textStyleAntiguo
maxWidth/maxHeight solos+ android:adjustViewBounds="true"Faltaba
Sin contentDescriptionAñadirlo en ImageView/ImageButtonAccesibilidad
switch(id){ case R.id.x: }if / else if (AGP 8+)Ya no compila
getResources().getColor()ContextCompat.getColor()Deprecated
Imágenes PNG en selector<shape> o vector drawableMejora
ListView / GridViewRecyclerView + LayoutManagerLegacy
SimpleCursorAdapterRoom + RecyclerViewLegacy
ToggleButtonMaterialSwitch / SwitchCompatAlternativa moderna

17 Chuleta final

ControlLeer / escribirListenerMétodo que salta
Button / ImageButton—setOnClickListeneronClick(View v)
ToggleButton / CheckBoxisChecked() / setChecked()setOnCheckedChangeListeneronCheckedChanged(CompoundButton, boolean)
RadioGroupgetCheckedRadioButtonId() / check() / clearCheck()setOnCheckedChangeListeneronCheckedChanged(RadioGroup, int checkedId)
TextViewgetText() / setText()——
EditTextgetText().toString() / setText()(addTextChangedListener)afterTextChanged(Editable)
ImageViewsetImageResource()——
SpinnergetSelectedItem() / setSelection()setOnItemSelectedListeneronItemSelected(...) + onNothingSelected(...)
ListView / GridViewsetAdapter()setOnItemClickListeneronItemClick(parent, view, position, id)
Layouts predefinidos para recordar
  • android.R.layout.simple_spinner_item → Spinner cerrado
  • android.R.layout.simple_spinner_dropdown_item → Spinner abierto (setDropDownViewResource)
  • android.R.layout.simple_list_item_1 → filas de ListView / celdas de GridView

Resumen de “Controls” — DAM M0489, INS Marianao. Código revisado y actualizado (Android Studio actual, AGP 8+, tema Material 3).