class MainActivity : AppCompatActivity() {
// 1. Referencias a los componentes del XML (se rellenan en onCreate)
private lateinit var etUsername: EditText
private lateinit var btnCheck: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) // qué XML pinta esta pantalla
initViews()
setupListeners()
}
// 2. Buscar los componentes por su id
private fun initViews() {
etUsername = findViewById(R.id.editTextText2)
btnCheck = findViewById(R.id.button)
}
// 3. Qué pasa al pulsar
private fun setupListeners() {
btnCheck.setOnClickListener { checkLogin() }
}
}
lateinit var"la inicializo más tarde" (en onCreate). Si la usas antes → UninitializedPropertyAccessException.
setContentView(R.layout.x)asocia el XML. Siempre antes de los findViewById, si no devuelven null y peta.
findViewById<Button>(R.id.x)dos formas: con tipo en la variable (val b: Button = findViewById(...)) o con genérico findViewById<Button>(...).
val local vs lateinit varEn P3.1.a se usan val dentro de onCreate (más corto). Si otra función necesita la vista → lateinit var de clase.
Registrar la Activity en el Manifest
Si creas la Activity con File ▸ New ▸ Activity ▸ Empty Views Activity, Android Studio la añade solo. Si la creas a mano, añádela tú o tendrás ActivityNotFoundException.
xmlAndroidManifest.xml
<application ... android:theme="@style/Theme.Author">
<!-- Pantalla secundaria: no se abre desde fuera de la app -->
<activity
android:name=".AuthorActivity"
android:exported="false" />
<!-- Pantalla de inicio: MAIN + LAUNCHER = icono en el móvil -->
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
Ciclo de vida (lo básico)
onCreate→onStart→onResume→▶ en uso→onPause→onStop→onDestroy
Al girar la pantalla la Activity se destruye y se recrea. Por eso en Ex4 y P3.1 los TextView de resultado llevan android:freezesText="true": así conservan el texto al rotar.
🎯 Para el examen
Las EditText guardan su texto al rotar solas (si tienen id). Los TextView NO, a menos que pongas freezesText="true".
📐Layouts: ConstraintLayout
Todas las prácticas usan ConstraintLayout: cada vista se "ata" con constraints a los bordes del padre o a otras vistas.
Las constraints que más usas
Atributo
Significa
app:layout_constraintTop_toTopOf="parent"
mi arriba pegado al arriba del padre
app:layout_constraintTop_toBottomOf="@id/x"
me pongo debajo de x
app:layout_constraintBottom_toTopOf="@id/x"
me pongo encima de x
Start_toStartOf="parent" + End_toEndOf="parent"
centrado horizontal
Top_toTopOf="parent" + Bottom_toBottomOf="parent"
centrado vertical
Start_toEndOf="@id/x"
me pongo a la derecha de x (Ex4: Cancel al lado de Submit)
Top_toTopOf="@id/x"
alineado arriba con x (misma fila)
app:layout_constraintVertical_chainStyle="packed"
cadena vertical: el grupo de vistas va junto y centrado
app:layout_constraintHorizontal_bias="0.5"
0 = izquierda, 1 = derecha, 0.5 = centro
Cadena vertical centrada (Ex1, Ex2, Ex3, Menús)
Para que 3 vistas queden juntas en el centro de la pantalla: la primera se ata al parent arriba, la última al parent abajo, y entre ellas se atan mutuamente. En la primera pones chainStyle="packed".
xmlactivity_info.xml (Ex1)
<TextView
android:id="@+id/tvSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is an app developed by"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/tvAuthorName"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/tvAuthorName"
...
android:layout_marginTop="12dp"
app:layout_constraintTop_toBottomOf="@+id/tvSubtitle"
app:layout_constraintBottom_toTopOf="@+id/btnClose" />
<Button
android:id="@+id/btnClose"
...
app:layout_constraintTop_toBottomOf="@+id/tvAuthorName"
app:layout_constraintBottom_toBottomOf="parent" />
Tamaños
wrap_contentlo justo para el contenido
match_parenttodo el ancho/alto del padre
0dp (match constraint)ocupa el espacio entre sus constraints. Lo usas en ListView/EditText que van de borde a borde.
dp vs spdp para tamaños y márgenes, sp para texto (respeta el tamaño de letra del usuario).
ScrollView (P3.1.a / P3.1.b)
Para que el formulario se pueda desplazar si no cabe (p. ej. en horizontal). Solo puede tener UN hijo → dentro va el ConstraintLayout con todo.
Fuente personalizada: selecciona el TextView ▸ atributo fontFamily ▸ More Fonts… ▸ elige ▸ Create downloadable font. Android Studio crea res/font/xxx.xml, font_certs.xml, preloaded_fonts.xml y el meta-data del Manifest.
🧱Plantillas de layouts
Tres o cuatro "esqueletos" que copias, pegas y borras lo que sobra. Todos tienen la Toolbar con el menú ⋮ ya integrada y los mismos ids, así que el Kotlin te vale para cualquiera.
El truco de todas: el esqueleto
LinearLayout vertical · match_parent · id=main
Toolbar · alto ?attr/actionBarSize → fija arriba, con el menú ⋮
ScrollView · height=0dp + weight=1 → "todo lo que queda"
LinearLayout vertical oConstraintLayout → UN solo hijo
¿Por qué el root es un LinearLayout y no un ConstraintLayout? Porque así la Toolbar queda arriba y el ScrollView ocupa el resto con weight, sin una sola constraint. La Toolbar no se mueve al hacer scroll.
¿Cuál uso?
1 · Formulario (Linear)
Mi App🔍 ℹ ⋮
Título
Nombre
Número
Opción A ▾
◉ Opción 1 ○ Opción 2
☑ Check 1 ☐ Check 2
ACEPTAR
LIMPIAR
Resultado
⇕ scroll
plantilla_scroll_linear.xml La más fácil. Para cualquier pantalla con campos y botones.
2 · Formulario (Constraint)
Mi App🔍 ℹ ⋮
Título
Nombre
Número
Opción A ▾
◉ Opción 1 ○ Opción 2
☑ Check 1 ☐ Check 2
ACEPTAR
LIMPIAR
Resultado
⇕ scroll
plantilla_scroll_constraint.xml Lo mismo con ConstraintLayout, por si te lo piden.
3 · Pantalla centrada
←Detalle🔍 ℹ ⋮
Bienvenido
Marouan
VOLVER
plantilla_centrada.xml Welcome / Author / Score: textos + botón en el centro, con flecha ←.
4 · Lista
Lista🔍 ℹ ⋮
Selecciona un elemento
Elemento 1
Elemento 2
Elemento 3
Elemento 4
Elemento 5
Elemento 6
plantilla_lista.xml ListView ocupando el resto. Sin ScrollView.
LinearLayout en 1 minuto
android:orientationvertical = uno debajo de otro · horizontal = uno al lado de otro. Si no lo pones, es horizontal (error típico: todo sale en una fila).
layout_weight="1"reparte el espacio que sobra. Pon a 0dp la medida en la dirección de la orientación (vertical → height=0dp, horizontal → width=0dp).
gravitycoloca a los hijos dentro de mí (center, end…)
layout_gravityme coloca a mí dentro de mi padre
padding / layout_marginpadding = espacio por dentro · margin = espacio por fuera
AnidarPara poner cosas en fila dentro de un Linear vertical → mete un LinearLayout horizontal dentro (como los 2 botones).
Dos botones mitad y mitadLinearLayout horizontal + cada botón width=0dp y weight=1.
Un botón 2/3 y otro 1/3weight=2 y weight=1.
Plantilla 1 · Toolbar + Scroll + LinearLayout
xmlres/layout/plantilla_scroll_linear.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<!-- ===== TOOLBAR (con el menú ⋮ integrado) ===== -->
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.Material3.Light"
app:title="Mi App"
app:titleTextColor="?attr/colorOnPrimary" />
<!-- ===== ZONA CON SCROLL: height 0dp + weight 1 = "todo lo que queda" ===== -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true">
<!-- El ScrollView solo admite UN hijo: este LinearLayout -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Título -->
<TextView
android:id="@+id/tvTitulo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center"
android:text="Título de la pantalla"
android:textSize="24sp"
android:textStyle="bold" />
<!-- Texto -->
<EditText
android:id="@+id/etNombre"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nombre"
android:inputType="text" />
<!-- Número (decimal y con signo) -->
<EditText
android:id="@+id/etNumero"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Número"
android:inputType="numberDecimal|numberSigned" />
<!-- Desplegable: las opciones salen de res/values/arrays.xml (sin adapter) -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Elige una opción:" />
<Spinner
android:id="@+id/spOpciones"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:entries="@array/opciones" />
<!-- Radio: solo una opción -->
<RadioGroup
android:id="@+id/rgOpciones"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:checkedButton="@id/rbOpcion1"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rbOpcion1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Opción 1" />
<RadioButton
android:id="@+id/rbOpcion2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="Opción 2" />
</RadioGroup>
<!-- Check: varias a la vez (en fila) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<CheckBox
android:id="@+id/cbOpcion1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Check 1" />
<CheckBox
android:id="@+id/cbOpcion2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="Check 2" />
</LinearLayout>
<!-- Dos botones en la misma fila, mitad y mitad (width 0dp + weight 1) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<Button
android:id="@+id/btnAceptar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Aceptar" />
<Button
android:id="@+id/btnLimpiar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:text="Limpiar" />
</LinearLayout>
<!-- Resultado (freezesText = no se pierde al girar) -->
<TextView
android:id="@+id/tvResultado"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:freezesText="true"
android:gravity="center"
android:text="Resultado"
android:textSize="18sp" />
</LinearLayout>
</ScrollView>
</LinearLayout>
Plantilla 2 · Toolbar + Scroll + ConstraintLayout
Regla para no liarte: cada vista Top_toBottomOf la anterior + width=0dp con Start/End al parent (ocupa todo el ancho). Para ponerla al lado de otra: Start_toEndOf + Top_toTopOf.
val texto = spOpciones.selectedItem.toString() // "Opción B"
val pos = spOpciones.selectedItemPosition // 1
spOpciones.setSelection(0) // volver a la primera
Menú genérico (iconos + ⋮ + submenú)
Los iconos @android:drawable/ic_menu_search, ic_menu_info_details, ic_menu_add, ic_menu_delete, ic_menu_edit, ic_menu_share… ya vienen con Android: no tienes que crear Vector Assets.
xmlres/menu/menu_main.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- ===== Iconos en la Toolbar ===== -->
<item
android:id="@+id/action_buscar"
android:icon="@android:drawable/ic_menu_search"
android:title="Buscar"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_info"
android:icon="@android:drawable/ic_menu_info_details"
android:title="Información"
app:showAsAction="ifRoom" />
<!-- ===== Dentro del ⋮ ===== -->
<item
android:id="@+id/action_pantalla2"
android:title="Ir a pantalla 2"
app:showAsAction="never" />
<item
android:id="@+id/action_web"
android:title="Abrir web"
app:showAsAction="never" />
<!-- Submenú: al pulsar "Más opciones" se abre otro desplegable -->
<item
android:id="@+id/action_mas"
android:title="Más opciones"
app:showAsAction="never">
<menu>
<item
android:id="@+id/action_sub1"
android:title="Subopción 1" />
<item
android:id="@+id/action_sub2"
android:title="Subopción 2" />
</menu>
</item>
<item
android:id="@+id/action_salir"
android:title="Salir"
app:showAsAction="never" />
</menu>
Kotlin de la plantilla 1 / 2 (Toolbar + menú + leer todos los controles)
kotlinMainActivity.kt
package cat.institutmarianao.examen // ← cambia por el package de TU proyecto
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.AdapterView
import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
/**
* PLANTILLA de Activity para plantilla_scroll_linear.xml o plantilla_scroll_constraint.xml
* (las dos tienen los mismos ids). Borra lo que no uses.
*/
class MainActivity : AppCompatActivity() {
companion object {
const val EXTRA_NOMBRE = "EXTRA_NOMBRE"
}
private lateinit var etNombre: EditText
private lateinit var etNumero: EditText
private lateinit var spOpciones: Spinner
private lateinit var rgOpciones: RadioGroup
private lateinit var rbOpcion1: RadioButton
private lateinit var cbOpcion1: CheckBox
private lateinit var cbOpcion2: CheckBox
private lateinit var btnAceptar: Button
private lateinit var btnLimpiar: Button
private lateinit var tvResultado: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.plantilla_scroll_linear) // ← nombre de tu XML
setupInsets()
setupToolbar()
initViews()
setupListeners()
}
// Que la Toolbar no quede debajo de la barra de estado (necesita android:id="@+id/main")
private fun setupInsets() {
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(bars.left, bars.top, bars.right, bars.bottom)
insets
}
}
// La Toolbar del XML pasa a ser la barra de la app (el tema debe ser NoActionBar)
private fun setupToolbar() {
val toolbar: Toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.title = "Mi App"
}
private fun initViews() {
etNombre = findViewById(R.id.etNombre)
etNumero = findViewById(R.id.etNumero)
spOpciones = findViewById(R.id.spOpciones)
rgOpciones = findViewById(R.id.rgOpciones)
rbOpcion1 = findViewById(R.id.rbOpcion1)
cbOpcion1 = findViewById(R.id.cbOpcion1)
cbOpcion2 = findViewById(R.id.cbOpcion2)
btnAceptar = findViewById(R.id.btnAceptar)
btnLimpiar = findViewById(R.id.btnLimpiar)
tvResultado = findViewById(R.id.tvResultado)
}
private fun setupListeners() {
btnAceptar.setOnClickListener { aceptar() }
btnLimpiar.setOnClickListener { limpiar() }
// (Opcional) reaccionar al cambiar el Spinner
spOpciones.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
// position = 0, 1, 2... | parent?.getItemAtPosition(position) = el texto
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
// (Opcional) reaccionar al marcar un CheckBox
cbOpcion1.setOnCheckedChangeListener { _, marcado -> /* marcado = true/false */ }
}
private fun aceptar() {
// ---- Leer EditText ----
val nombre = etNombre.text.toString().trim()
if (nombre.isEmpty()) {
etNombre.error = "Campo obligatorio"
return
}
val numero = etNumero.text.toString().toDoubleOrNull() // null si está vacío o mal
// ---- Leer Spinner ----
val opcionSpinner = spOpciones.selectedItem.toString()
val posSpinner = spOpciones.selectedItemPosition
// ---- Leer RadioGroup ----
val radioMarcado = findViewById<RadioButton>(rgOpciones.checkedRadioButtonId).text
// o: if (rbOpcion1.isChecked) ... else ...
// ---- Leer CheckBox ----
val checks = mutableListOf<String>()
if (cbOpcion1.isChecked) checks.add("Check 1")
if (cbOpcion2.isChecked) checks.add("Check 2")
tvResultado.text = "Nombre: $nombre\nNúmero: ${numero ?: "-"}\n" +
"Spinner: $opcionSpinner ($posSpinner)\nRadio: $radioMarcado\n" +
"Checks: ${checks.joinToString()}"
}
private fun limpiar() {
etNombre.text.clear()
etNumero.text.clear()
spOpciones.setSelection(0)
rbOpcion1.isChecked = true
cbOpcion1.isChecked = false
cbOpcion2.isChecked = false
tvResultado.text = "Resultado"
}
// ================== MENÚ DE LA TOOLBAR ==================
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_pantalla2 -> {
val intent = Intent(this, SegundaActivity::class.java)
intent.putExtra(EXTRA_NOMBRE, etNombre.text.toString())
startActivity(intent)
}
R.id.action_web -> {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://www.google.com")))
}
R.id.action_mas -> { /* abre el submenú solo, no hay que hacer nada */ }
R.id.action_salir -> confirmarSalir()
else -> Toast.makeText(this, item.title.toString(), Toast.LENGTH_SHORT).show()
}
return true
}
private fun confirmarSalir() {
AlertDialog.Builder(this)
.setTitle("Salir")
.setMessage("¿Seguro que quieres salir?")
.setPositiveButton("Sí") { _, _ -> finish() }
.setNegativeButton("No", null)
.show()
}
}
Kotlin de la plantilla 3 (con flecha ← en la Toolbar)
kotlinSegundaActivity.kt
package cat.institutmarianao.examen // ← cambia por el package de TU proyecto
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
/**
* PLANTILLA de segunda pantalla para plantilla_centrada.xml
* - Toolbar con flecha ← para volver
* - Recibe un dato por Intent
* Recuerda: debe estar en el AndroidManifest (si la creas con New ▸ Activity se añade sola).
*/
class SegundaActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.plantilla_centrada)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(bars.left, bars.top, bars.right, bars.bottom)
insets
}
// Toolbar con flecha de volver
val toolbar: Toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.title = "Detalle"
supportActionBar?.setDisplayHomeAsUpEnabled(true) // muestra la flecha ←
toolbar.setNavigationOnClickListener { finish() } // qué hace la flecha
// Dato recibido
val nombre = intent.getStringExtra(MainActivity.EXTRA_NOMBRE) ?: "Usuario"
findViewById<TextView>(R.id.tvLinea2).text = nombre
findViewById<Button>(R.id.btnVolver).setOnClickListener { finish() }
}
}
Kotlin de la plantilla 4
kotlinListaActivity.kt
package cat.institutmarianao.examen // ← cambia por el package de TU proyecto
import android.os.Bundle
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
/** PLANTILLA para plantilla_lista.xml (Toolbar + ListView) */
class ListaActivity : AppCompatActivity() {
private val elementos = listOf("Elemento 1", "Elemento 2", "Elemento 3", "Elemento 4")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.plantilla_lista)
setSupportActionBar(findViewById<Toolbar>(R.id.toolbar))
val tvInfo = findViewById<TextView>(R.id.tvInfo)
val lvLista = findViewById<ListView>(R.id.lvLista)
lvLista.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, elementos)
lvLista.setOnItemClickListener { _, _, position, _ ->
tvInfo.text = "Has pulsado: ${elementos[position]}"
}
}
}
Cómo meterlas en tu proyecto del examen
Crea la Activity con File ▸ New ▸ Activity ▸ Empty Views Activity (así se añade al Manifest).
Abre su XML (res/layout/activity_xxx.xml), pasa a la vista Code, borra todo y pega la plantilla.
Cambia tools:context=".MainActivity" por el nombre de tu Activity (solo afecta a la vista previa).
Si usas el Spinner con @array/opciones: crea res/values/arrays.xml.
Si usas menú: crea res/menu/menu_main.xml (clic derecho en res ▸ New ▸ Android Resource File ▸ tipo Menu).
En Kotlin pega solo los métodos que necesites, sin tocar la línea package de tu fichero. Revisa que setContentView(R.layout.…) apunta a tu XML.
Comprueba que el tema en res/values/themes.xml es ...NoActionBar (en los proyectos nuevos ya lo es).
Atajo: menú sin poner Toolbar
Si no quieres Toolbar en el XML, cambia el padre del tema de Theme.Material3.DayNight.NoActionBar a Theme.Material3.DayNight. Entonces la app trae su barra de serie y onCreateOptionsMenu funciona igual, sin setSupportActionBar.
Con el tema NoActionBar y sin Toolbar, supportActionBar es null: supportActionBar?.title = ... no hace nada y el menú no aparece. Con la Toolbar y el tema con barra → error "This Activity already has an action bar". Elige una de las dos opciones.
Errores típicos de layouts
Todo sale en una filaFalta android:orientation="vertical" en el LinearLayout.
"ScrollView can host only one direct child"Has metido 2 vistas en el ScrollView → envuélvelas en un LinearLayout.
El scroll no deja ver lo de abajo / tapa la ToolbarEl ScrollView debe ir con height=0dp + weight=1 (o match_parent si no hay Toolbar).
El weight no reparte bienTe falta poner a 0dp el ancho (horizontal) o el alto (vertical).
En Constraint todo sale arriba a la izquierdaA la vista le faltan constraints (sale el aviso rojo "not constrained").
La Toolbar queda debajo de la hora/bateríaFalta el bloque de WindowInsets de enableEdgeToEdge, o el root no tiene android:id="@+id/main".
Iconos del menú en negro sobre azulFalta android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar" en la Toolbar.
Crash al leer el RadioGroupcheckedRadioButtonId es -1 si no hay ninguno marcado → pon android:checkedButton.
🎛️Controles básicos
TextView · EditText · Button
kotlinEditText / TextView
// Leer lo que ha escrito el usuario
val username = etUsername.text.toString().trim() // trim() quita espacios
val password = etPassword.text.toString()
// Validar vacío y mostrar error dentro del propio EditText
if (username.isEmpty()) {
etUsername.error = "Introduce un nombre de usuario !!!"
return
}
// Escribir en un TextView
tvWelcome.text = "Welcome $username" // plantilla de String con $
// Vaciar un EditText
etNumber1.text.clear()
// Cambiar el texto de un botón
btnCalculate.text = "Add & Subtract"
Convertir texto a número sin que pete
kotlinP3.1.a / P3.1.b
val num1 = etNumber1.text.toString().toDoubleOrNull() // null si no es número
val num2 = etNumber2.text.toString().toDoubleOrNull()
if (num1 == null || num2 == null) {
Toast.makeText(this, "Introduce los dos números", Toast.LENGTH_SHORT).show()
return@setOnClickListener // salir de la lambda (o return si es una función)
}
⚠️ Ojo
toDouble() lanza excepción con texto vacío. Usa toDoubleOrNull() / toIntOrNull(). Dentro de un setOnClickListener { } se sale con return@setOnClickListener.
Visibility
View.VISIBLEse ve
View.INVISIBLEno se ve, pero ocupa su hueco
View.GONEno se ve y no ocupa espacio
kotlinEx2 Password
tvError.text = "Wrong password.\n It's so easy as abc123"
tvError.visibility = View.VISIBLE // mostrar
tvError.visibility = View.GONE // ocultar
Escuchar mientras se escribe — doOnTextChanged (Ex3)
// Se ejecuta cada vez que se marca o desmarca
cbAdd.setOnCheckedChangeListener { _, _ -> updateButton() }
cbSubstract.setOnCheckedChangeListener { _, _ -> updateButton() }
private fun updateButton() {
btnCalculate.text = when {
cbAdd.isChecked && cbSubstract.isChecked -> "Add & Subtract"
cbAdd.isChecked -> "Add"
cbSubstract.isChecked -> "Subtract"
else -> "Calculate"
}
}
RadioButtonExcluyentes (1 de N). Van dentro de un RadioGroup.
CheckBoxIndependientes (0..N). No necesitan grupo.
Toast
kotlinToast
Toast.makeText(this, "Texto", Toast.LENGTH_SHORT).show() // ¡no olvides .show()!
Toast.makeText(this, getString(R.string.toast_ocultar), Toast.LENGTH_LONG).show()
// Dentro de un object anónimo (Spinner) "this" no es la Activity:
Toast.makeText(this@MainActivity, "...", Toast.LENGTH_SHORT).show()
AlertDialog de confirmación (Ex2, Ex3, Ex4)
kotlinAlertDialog
// import androidx.appcompat.app.AlertDialog
AlertDialog.Builder(this)
.setTitle("Confirmación")
.setMessage("¿Estás seguro de que deseas salir?")
.setPositiveButton("Sí") { _, _ -> finish() }
.setNegativeButton("No", null) // null = solo cierra el diálogo
.show()
🔀Intents: navegar y pasar datos
Un Intent es un "mensaje" para abrir otra pantalla (o app). Hay 4 casos en tus prácticas:
A Abrir y ya
B Abrir + enviar datos
C Abrir y que me devuelva datos
D Intent implícito (web)
A · Abrir otra Activity y cerrarla (Ex1 Author)
kotlinIntent explícito
// MainActivity
val intent = Intent(this, AuthorActivity::class.java)
startActivity(intent)
// AuthorActivity → botón CLOSE
btnClose.setOnClickListener { finish() } // destruye esta y vuelve a la anterior
⚠️ Ojo
Para volver usa finish(), no otro startActivity hacia la Main: eso apilaría pantallas (Main → Author → Main → Author…).
B · Enviar datos con putExtra (Ex2, Ex3)
kotlinEmisor — MainActivity (Ex3)
class MainActivity : AppCompatActivity() {
// Claves en un companion object → no te equivocas al escribirlas
companion object {
const val USER_NAME = "USER_NAME"
const val USER_SCORE = "USER_SCORE"
}
private fun openScoreActivity(user: String, score: Int) {
val intent = Intent(this, ScoreActivity::class.java).apply {
putExtra(USER_NAME, user)
putExtra(USER_SCORE, score)
}
startActivity(intent)
}
}
kotlinReceptor — ScoreActivity (Ex3)
// Receptor — ScoreActivity
val username = intent.getStringExtra(MainActivity.USER_NAME) ?: "User"
val score = intent.getIntExtra(MainActivity.USER_SCORE, 0) // 0 = valor por defecto
tvWelcome.text = "Welcome my lord, $username!"
tvScore.text = "Your score is $score"
getStringExtra(k)devuelve String? → usa ?: "valor" (operador Elvis)
getIntExtra(k, def)obliga a dar valor por defecto
getBooleanExtra / getDoubleExtraigual que Int, con defecto
.apply { }configura el objeto y lo devuelve (ahorra repetir intent.)
C · Que la 2ª pantalla DEVUELVA un dato (Ex4 Name or age) ⭐
Main: launcher.launch(intent)
Data: setResult(RESULT_OK, intent) + finish()
Main: se ejecuta la lambda del launcher
kotlinMainActivity — quien pide el dato
class MainActivity : AppCompatActivity() {
private lateinit var textView3: TextView
// Se declara como propiedad (fuera de onCreate)
private val launcher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val tipus = result.data?.getStringExtra("TIPUS")
val dada = result.data?.getStringExtra("DADA")
textView3.text = "The $tipus entered is $dada"
}
}
override fun onCreate(savedInstanceState: Bundle?) {
...
button.setOnClickListener {
val intent = Intent(this, DataActivity::class.java)
intent.putExtra("TIPUS", "name")
launcher.launch(intent) // en vez de startActivity
}
}
}
kotlinDataActivity — quien devuelve el dato
// SUBMIT: devolver el dato
private fun submit() {
val resultat = Intent()
resultat.putExtra(TIPUS, tipus)
resultat.putExtra(DADA, editText.text.toString())
setResult(Activity.RESULT_OK, resultat)
finish()
}
// CANCEL: salir sin dato
private fun exitWithoutSaving() {
setResult(Activity.RESULT_CANCELED)
finish()
}
🎯 Para el examen
registerForActivityResult debe registrarse antes de que la Activity se cree → como propiedad de la clase (private val launcher = ...), nunca dentro de un click. Imports: androidx.activity.result.contract.ActivityResultContracts y android.app.Activity.
D · Intent implícito: abrir una web (Menús)
kotlinAndroid elige qué app lo abre (navegador)
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://one.google.com/about/plans"))
startActivity(intent)
Interceptar el botón Atrás del sistema (Ex3)
kotlinScoreActivity — en onCreate
// import androidx.activity.OnBackPressedCallback
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
showConfirmationDialog() // pregunta antes de salir
}
})
💡 Truco
onBackPressed() está deprecated. La forma moderna es onBackPressedDispatcher.addCallback.
📋ListView y Spinner (Adapters)
Ambos muestran una lista de datos. El ArrayAdapter es el "traductor": convierte cada elemento de la lista en una fila visual.
Datos List<String>
ArrayAdapter + layout de fila
ListView / Spinner .adapter = adapter
Los datos (clase Java AmericanCountries)
javaDos listas paralelas: misma posición = mismo país
public class AmericanCountries {
public static final List<String> COUNTRIES = new ArrayList<>();
public static final List<Integer> AREAS = new ArrayList<>();
static { // bloque estático: se ejecuta una vez
COUNTRIES.add("Canadá"); AREAS.add(9984670);
COUNTRIES.add("Brasil"); AREAS.add(8514877);
...
}
}
Desde Kotlin se usan directamente: AmericanCountries.COUNTRIES[position]. Kotlin y Java conviven en el mismo proyecto sin problema.
Se llama solo a onItemSelected(0) → ya muestra el primero
⚠️ Ojo
En el object del Spinner, this es el listener, no la Activity → usa this@MainActivity para el Toast.
💡 Truco
Si te piden un array fijo en vez de la clase Java: en Kotlin val paises = arrayOf("A","B") o en res/values/strings.xml con <string-array> y resources.getStringArray(R.array.paises).
☰Toolbar y menús
Práctica Pt1.2 Menús: Toolbar propia, iconos en la barra, menú desplegable ⋮, abrir otra pantalla y una web.
1 · Tema NoActionBar
2 · Toolbar en el XML
3 · setSupportActionBar
4 · res/menu/menu_main.xml
5 · onCreateOptionsMenu
6 · onOptionsItemSelected
1 · Tema sin ActionBar (si no, salen 2 barras o peta)
Crear: clic derecho en res/values ▸ New ▸ Values Resource File ▸ nombre strings ▸ Qualifier Locale ▸ idioma. O usa el Translations Editor (botón "Open editor" arriba de strings.xml). El & se escribe &.
// Arrays paralelos (Ex3)
private val users = arrayOf("gandalf", "frodo", "saruman")
private val points = intArrayOf(315, 222, 489)
// Buscar posición ignorando mayúsculas (-1 si no está)
val i = users.indexOfFirst { it.equals(user, ignoreCase = true) }
return if (i != -1) points[i] else 0 // if como expresión
// Elvis: valor por defecto si es null
val name = intent.getStringExtra("K") ?: "User"
// Safe call: solo llama si no es null
supportActionBar?.hide()
result.data?.getStringExtra("DADA")
// Plantillas de String
"Add: ${add(num1, num2)}\n"
// Constantes de clase
companion object { const val USER_NAME = "USER_NAME" }
// Argumentos de lambda que no usas → _
cbAdd.setOnCheckedChangeListener { _, _ -> updateButton() }
Edge-to-edge (código que genera Android Studio)
Las plantillas nuevas ponen enableEdgeToEdge() y un listener de insets. Necesita que el layout raíz tenga android:id="@+id/main".