App Android con una API Rest en Laravel
¿Quieres crear una aplicación móvil que permita a los usuarios publicar contenido y ver las últimas publicaciones en tiempo real? En este tutorial, te guiaré paso a paso para desarrollar una solución completa: una API REST con Laravel como backend y una app Android que consuma esos datos. Desde configurar el servidor hasta mostrar publicaciones en una lista, aprenderás a conectar ambas partes de forma sencilla y efectiva. ¡Perfecto para desarrolladores que buscan dominar el desarrollo full-stack!
Índice
- ¿Por Qué Combinar Laravel y Android Studio?
- Requisitos Previos
- Desarrollo de la API REST con Laravel
- Paso 1: Instalar Laravel
- Paso 2: Configurar la Base de Datos
- Paso 3: Crear el Modelo y Migración
- Paso 4: Definir Rutas y Controlador
- Desarrollo de la App Android
- Paso 5: Configurar el Proyecto Android
- Paso 6: Agregar Dependencias y Permisos
- Paso 7: Diseñar la Interfaz de Usuario
- Paso 8: Implementar Lógica con Retrofit
- Código Completo y Estructura
- Mejoras y Buenas Prácticas
- Conclusión: Tu App Full-Stack Lista
¿Por Qué Combinar Laravel y Android Studio?
Laravel y Android Studio forman una dupla poderosa para desarrollar aplicaciones móviles dinámicas:
- Laravel: Framework PHP robusto para crear APIs REST rápidas y seguras.
- Android Studio: Entorno ideal para apps Android con soporte para Retrofit y SQLite.
- Beneficios: Escalabilidad, acceso a datos en tiempo real y una arquitectura cliente-servidor sólida.
Este tutorial te enseñará a construir una app donde los usuarios puedan publicar mensajes y ver un feed actualizado, perfecta para redes sociales, blogs o sistemas de comentarios.
Requisitos Previos
- Laravel: PHP 7.4+, Composer, y un servidor local (XAMPP, WAMP).
- Android Studio: Versión reciente con emulador o dispositivo físico.
- Conocimientos básicos: PHP, Java, XML y conceptos de REST.
- Base de datos: MySQL configurada.
Desarrollo de la API REST con Laravel
Paso 1: Instalar Laravel
Instalamos Laravel en el directorio de proyectos de XAMPP con Composer:
bash
composer create-project laravel/laravel arequipa --prefer-dist
-
--prefer-dist: Usa una versión precompilada para una instalación más rápida.
- Resultado: Se crea el proyecto arequipa con la última versión estable.
Paso 2: Configurar la Base de Datos
Editamos el archivo .env para conectar con MySQL:
text
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=arequipa
DB_USERNAME=root
DB_PASSWORD=
Limitamos la longitud de los campos varchar en app/Providers/AppServiceProvider.php:
php
namespace App\Providers; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\Schema;class AppServiceProvider extends ServiceProvider { public function boot() { Schema::defaultStringLength(191); } }
- Por qué: Evita errores de longitud en índices de MySQL.
Paso 3: Crear el Modelo y Migración
Generamos el modelo Publicaciones con su migración:
bash
php artisan make:model Publicaciones -m
Editamos app/Models/Publicaciones.php:
php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Publicaciones extends Model {
protected $fillable = ['nombre', 'descripcion'];
}
Configuramos la migración en database/migrations/xxx_create_publicaciones_table.php:
php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePublicacionesTable extends Migration {
public function up() {
Schema::create('publicaciones', function (Blueprint $table) {
$table->id();
$table->string('nombre', 25);
$table->string('descripcion', 190);
$table->timestamps();
});
}
public function down() {
Schema::dropIfExists('publicaciones');
}
}
Ejecutamos la migración:
bash
php artisan migrate
Paso 4: Definir Rutas y Controlador
En routes/api.php, añadimos una ruta resource:
php
use App\Http\Controllers\PublicacionesController;
use Illuminate\Support\Facades\Route;
Route::resource('publicaciones', PublicacionesController::class)->only(['index', 'store']);
Creamos el controlador:
bash
php artisan make:controller PublicacionesController
Editamos app/Http/Controllers/PublicacionesController.php:
php
namespace App\Http\Controllers;
use App\Models\Publicaciones;
use Illuminate\Http\Request;
class PublicacionesController extends Controller {
public function index() {
$publicaciones = Publicaciones::orderBy('created_at', 'desc')->get();
return response()->json($publicaciones, 200);
}
public function store(Request $request) {
$publicacion = new Publicaciones($request->only('nombre', 'descripcion'));
$publicacion->save();
return response()->json($publicacion, 201);
}
}
- index: Devuelve todas las publicaciones en JSON, ordenadas por fecha descendente.
- store: Inserta una nueva publicación y retorna el registro creado.
Probamos la API:
bash
php artisan serve
URL de prueba: http://127.0.0.1:8000/api/publicaciones
Desarrollo de la App Android
Paso 5: Configurar el Proyecto Android
En Android Studio, creamos un nuevo proyecto llamado "Arequipa" con una actividad vacía.
Paso 6: Agregar Dependencias y Permisos
En app/build.gradle, añadimos:
gradle
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'androidx.cardview:cardview:1.0.0'
}
En AndroidManifest.xml, habilitamos internet:
xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<activity android:name=".PublicacionesActivity"/>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- usesCleartextTraffic: Permite tráfico HTTP no cifrado (solo para pruebas).
Paso 7: Diseñar la Interfaz de Usuario
res/layout/activity_main.xml (Portada):
xml
<androidx.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F5F5F5">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_logo"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/btnEntrar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<Button
android:id="@+id/btnEntrar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Entrar"
android:backgroundTint="#0288D1"
android:textColor="#FFFFFF"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="120dp"/>
</androidx.constraint.ConstraintLayout>
res/layout/activity_publicaciones.xml (Publicaciones):
xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:elevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<EditText
android:id="@+id/nombre"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nombre"/>
<EditText
android:id="@+id/descripcion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Descripción"/>
<Button
android:id="@+id/btnSave"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Publicar"
android:layout_gravity="end"
android:backgroundTint="#0288D1"
android:textColor="#FFFFFF"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<ListView
android:id="@+id/lista"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
res/layout/lista.xml (Plantilla para ListView):
xml
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:elevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/txtnombre"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"/>
<TextView
android:id="@+id/txtdescripcionn"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
Paso 8: Implementar Lógica con Retrofit
Config.java:
java
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import android.content.Context;
import android.widget.Toast;
public class Config {
private static final String BASE_URL = "http://127.0.0.1:8000/api/";
private static Retrofit retrofit;
public static Retrofit getRetrofit() {
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
public static void mensaje(Context context, String texto) {
Toast.makeText(context, texto, Toast.LENGTH_SHORT).show();
}
}
ApiService.java (Interfaz Retrofit):
java
import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.POST;
import java.util.List;
public interface ApiService {
@GET("publicaciones")
Call<List<Publicaciones>> getPublicaciones();
@POST("publicaciones")
@FormUrlEncoded
Call<Publicaciones> savePublicacion(
@Field("nombre") String nombre,
@Field("descripcion") String descripcion
);
}
Publicaciones.java (Modelo):
java
public class Publicaciones {
private int id;
private String nombre;
private String descripcion;
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getNombre() { return nombre; }
public void setNombre(String nombre) { this.nombre = nombre; }
public String getDescripcion() { return descripcion; }
public void setDescripcion(String descripcion) { this.descripcion = descripcion; }
}
MainActivity.java:
java
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btnEntrar = findViewById(R.id.btnEntrar);
btnEntrar.setOnClickListener(v -> startActivity(new Intent(this, PublicacionesActivity.class)));
}
}
PublicacionesActivity.java:
java
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import java.util.List;
public class PublicacionesActivity extends AppCompatActivity {
private EditText nombre, descripcion;
private Button btnSave;
private ListView lista;
private ApiService apiService = Config.getRetrofit().create(ApiService.class);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_publicaciones);
nombre = findViewById(R.id.nombre);
descripcion = findViewById(R.id.descripcion);
btnSave = findViewById(R.id.btnSave);
lista = findViewById(R.id.lista);
getPublicaciones();
btnSave.setOnClickListener(v -> {
String n = nombre.getText().toString().trim();
String d = descripcion.getText().toString().trim();
if (n.isEmpty()) {
Config.mensaje(this, "Ingrese un nombre");
} else if (d.isEmpty()) {
Config.mensaje(this, "Ingrese una descripción");
} else {
savePublicacion(n, d);
}
});
}
private void savePublicacion(String n, String d) {
Call<Publicaciones> call = apiService.savePublicacion(n, d);
call.enqueue(new Callback<Publicaciones>() {
@Override
public void onResponse(Call<Publicaciones> call, Response<Publicaciones> response) {
if (response.isSuccessful()) {
Config.mensaje(PublicacionesActivity.this, "Publicación guardada");
nombre.setText("");
descripcion.setText("");
getPublicaciones();
} else {
Config.mensaje(PublicacionesActivity.this, "Error al guardar");
}
}
@Override
public void onFailure(Call<Publicaciones> call, Throwable t) {
Config.mensaje(PublicacionesActivity.this, "Error de conexión: " + t.getMessage());
}
});
}
private void getPublicaciones() {
Call<List<Publicaciones>> call = apiService.getPublicaciones();
call.enqueue(new Callback<List<Publicaciones>>() {
@Override
public void onResponse(Call<List<Publicaciones>> call, Response<List<Publicaciones>> response) {
if (response.isSuccessful()) {
List<Publicaciones> publicaciones = response.body();
lista.setAdapter(new AdapterPublicaciones(PublicacionesActivity.this, publicaciones));
}
}
@Override
public void onFailure(Call<List<Publicaciones>> call, Throwable t) {
Config.mensaje(PublicacionesActivity.this, "Error al cargar: " + t.getMessage());
}
});
}
class AdapterPublicaciones extends ArrayAdapter<Publicaciones> {
private List<Publicaciones> publicaciones;
public AdapterPublicaciones(Context context, List<Publicaciones> publicaciones) {
super(context, R.layout.lista, publicaciones);
this.publicaciones = publicaciones;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.lista, parent, false);
}
TextView txtNombre = convertView.findViewById(R.id.txtnombre);
TextView txtDescripcion = convertView.findViewById(R.id.txtdescripcionn);
txtNombre.setText(publicaciones.get(position).getNombre());
txtDescripcion.setText(publicaciones.get(position).getDescripcion());
return convertView;
}
}
}
Mejoras y Buenas Prácticas
- Validación: Añade reglas en Laravel ($request->validate) para campos requeridos.
- Seguridad: Usa HTTPS en producción y tokens JWT para autenticación.
- Rendimiento: Implementa paginación en la API (Publicaciones::paginate(10)).
- Diseño: Personaliza CardView con colores y sombras consistentes.
- Errores: Maneja excepciones detalladamente en Retrofit y Laravel.
Conclusión: Tu App Full-Stack Lista
Con este tutorial, has creado una app móvil funcional que publica y muestra contenido en tiempo real usando Laravel y Android Studio. Desde una API REST sencilla hasta una interfaz Android con Retrofit, ahora tienes las bases para proyectos más complejos como redes sociales o foros. ¡Personaliza, escala y publica tu app! ¿Listo para más? Explora autenticación y notificaciones en nuestro próximo artículo.
Recurso del Tutorial
GratisTutoriales Recomendados
Sigue explorando publicaciones de la misma categoría