Android

App Android con una API Rest en Laravel

8 abril, 2025
19,674 lecturas
Cómo Desarrollar una App Móvil para Publicar y Mostrar Publicaciones

¿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

  1. ¿Por Qué Combinar Laravel y Android Studio?
  2. Requisitos Previos
  3. 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
  4. 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
  5. Código Completo y Estructura
  6. Mejoras y Buenas Prácticas
  7. 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

  1. Validación: Añade reglas en Laravel ($request->validate) para campos requeridos.
  2. Seguridad: Usa HTTPS en producción y tokens JWT para autenticación.
  3. Rendimiento: Implementa paginación en la API (Publicaciones::paginate(10)).
  4. Diseño: Personaliza CardView con colores y sombras consistentes.
  5. 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

Gratis
2 descargas

Tutoriales Recomendados

Sigue explorando publicaciones de la misma categoría

Ver todos