← Volver al listado de tecnologías

Arquitectura y Buenas Prácticas

Por: SiempreListo
kivyarquitecturamvctestingbuenas-practicas

Estructura de Proyecto

Proyecto Pequeño

mi_app/
├── main.py
├── main.kv
├── buildozer.spec
└── assets/
    ├── images/
    └── fonts/

Proyecto Mediano

mi_app/
├── main.py
├── app.kv
├── buildozer.spec
├── assets/
├── screens/
│   ├── __init__.py
│   ├── home.py
│   └── settings.py
├── widgets/
│   ├── __init__.py
│   └── custom_button.py
├── utils/
│   ├── __init__.py
│   └── helpers.py
└── services/
    ├── __init__.py
    └── api.py

Proyecto Grande

mi_app/
├── src/
│   ├── __init__.py
│   ├── main.py
│   ├── app.py
│   ├── screens/
│   ├── widgets/
│   ├── models/
│   ├── services/
│   ├── utils/
│   └── kv/
├── assets/
├── tests/
├── docs/
├── buildozer.spec
└── requirements.txt

Separar KV por Pantalla

Cargar múltiples archivos KV

# main.py
from kivy.lang import Builder

# Cargar todos los KV
Builder.load_file('kv/widgets.kv')
Builder.load_file('kv/screens/home.kv')
Builder.load_file('kv/screens/settings.kv')

Estructura KV

kv/
├── base.kv        # Estilos base
├── widgets.kv     # Widgets personalizados
└── screens/
    ├── home.kv
    └── settings.kv

Patrón MVC Adaptado

Model (datos)

# models/user.py
from kivy.properties import StringProperty, NumericProperty
from kivy.event import EventDispatcher

class User(EventDispatcher):
    nombre = StringProperty('')
    email = StringProperty('')
    edad = NumericProperty(0)

    def validar(self):
        return bool(self.nombre and self.email)

    def to_dict(self):
        return {
            'nombre': self.nombre,
            'email': self.email,
            'edad': self.edad
        }

Controller (lógica)

# controllers/user_controller.py
class UserController:
    def __init__(self, app):
        self.app = app
        self.user = User()

    def login(self, email, password):
        # Lógica de autenticación
        resultado = self.api.login(email, password)
        if resultado:
            self.user.email = email
            return True
        return False

    def logout(self):
        self.user = User()

View (pantalla)

# screens/login.py
from kivy.uix.screen import Screen

class LoginScreen(Screen):
    def intentar_login(self):
        email = self.ids.email.text
        password = self.ids.password.text

        if self.app.user_controller.login(email, password):
            self.manager.current = 'home'

Inyección de Dependencias

# services/api.py
class ApiService:
    def __init__(self, base_url):
        self.base_url = base_url

    def get(self, endpoint):
        pass

# main.py
class MiApp(App):
    def build(self):
        # Inyectar dependencias
        self.api = ApiService('https://api.ejemplo.com')
        self.user_controller = UserController(self, self.api)
        return RootWidget()

Manejo de Estado

Estado Global en App

class MiApp(App):
    usuario_actual = ObjectProperty(None, allownone=True)
    tema = StringProperty('light')
    items = ListProperty([])

    def on_usuario_actual(self, instance, value):
        # Reaccionar a cambios de usuario
        if value:
            self.cargar_datos_usuario()

Acceder desde cualquier parte

from kivy.app import App

def obtener_usuario():
    app = App.get_running_app()
    return app.usuario_actual

Persistencia de Datos

JSON Store

from kivy.storage.jsonstore import JsonStore

class DataManager:
    def __init__(self, filename='data.json'):
        self.store = JsonStore(filename)

    def guardar(self, key, data):
        self.store.put(key, **data)

    def obtener(self, key, default=None):
        if self.store.exists(key):
            return self.store.get(key)
        return default

    def eliminar(self, key):
        if self.store.exists(key):
            self.store.delete(key)

Configuración de App

from kivy.config import Config

# Antes de importar otros módulos kivy
Config.set('graphics', 'width', '400')
Config.set('graphics', 'height', '600')
Config.set('kivy', 'log_level', 'warning')

Manejo de Errores

from kivy.logger import Logger

class MiApp(App):
    def build(self):
        try:
            return self.construir_ui()
        except Exception as e:
            Logger.error(f'App: Error construyendo UI: {e}')
            return self.pantalla_error(str(e))

    def pantalla_error(self, mensaje):
        from kivy.uix.label import Label
        return Label(text=f'Error: {mensaje}')

Logger

from kivy.logger import Logger

Logger.debug('MiModulo: Mensaje debug')
Logger.info('MiModulo: Mensaje info')
Logger.warning('MiModulo: Mensaje warning')
Logger.error('MiModulo: Mensaje error')

Testing

Unittest

# tests/test_models.py
import unittest
from models.user import User

class TestUser(unittest.TestCase):
    def test_validar_usuario_vacio(self):
        user = User()
        self.assertFalse(user.validar())

    def test_validar_usuario_completo(self):
        user = User(nombre='Test', email='[email protected]')
        self.assertTrue(user.validar())

if __name__ == '__main__':
    unittest.main()

Pytest

# tests/test_user.py
import pytest
from models.user import User

@pytest.fixture
def user():
    return User(nombre='Test', email='[email protected]')

def test_to_dict(user):
    data = user.to_dict()
    assert data['nombre'] == 'Test'
    assert data['email'] == '[email protected]'

Async/Await

import asyncio
from kivy.app import async_runTouchApp

async def cargar_datos():
    await asyncio.sleep(1)  # Simular carga
    return {'data': 'valor'}

class MiApp(App):
    async def cargar_async(self):
        datos = await cargar_datos()
        self.procesar(datos)

# Ejecutar
if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(async_runTouchApp(MiApp().build()))

Threading para Operaciones Largas

from threading import Thread
from kivy.clock import mainthread

class MiWidget(Widget):
    def iniciar_descarga(self):
        Thread(target=self.descargar).start()

    def descargar(self):
        # Operación larga en hilo separado
        resultado = requests.get(url)
        self.actualizar_ui(resultado)

    @mainthread
    def actualizar_ui(self, datos):
        # Solo actualizar UI desde mainthread
        self.label.text = str(datos)

Internacionalización (i18n)

# utils/i18n.py
import json

class I18n:
    def __init__(self, lang='es'):
        self.lang = lang
        self.strings = self.cargar(lang)

    def cargar(self, lang):
        with open(f'locales/{lang}.json') as f:
            return json.load(f)

    def t(self, key, **kwargs):
        texto = self.strings.get(key, key)
        return texto.format(**kwargs) if kwargs else texto

# Uso
i18n = I18n('es')
print(i18n.t('welcome', name='Usuario'))
// locales/es.json
{
    "welcome": "Bienvenido, {name}",
    "logout": "Cerrar sesión"
}

Profiling

# Habilitar profiling
import os
os.environ['KIVY_PROFILE_LANG'] = '1'

from kivy.app import App

cProfile

python -m cProfile -o output.prof main.py
python -m pstats output.prof

Checklist de Producción

Testing de Arquitectura

# test_arquitectura.py
import unittest
from kivy.storage.jsonstore import JsonStore
import tempfile
import os

class TestJsonStore(unittest.TestCase):
    def setUp(self):
        self.temp_file = tempfile.mktemp(suffix='.json')
        self.store = JsonStore(self.temp_file)

    def tearDown(self):
        if os.path.exists(self.temp_file):
            os.remove(self.temp_file)

    def test_guardar_y_leer(self):
        self.store.put('usuario', nombre='Test', edad=25)
        self.assertTrue(self.store.exists('usuario'))
        self.assertEqual(self.store.get('usuario')['nombre'], 'Test')

    def test_eliminar(self):
        self.store.put('temp', valor=1)
        self.store.delete('temp')
        self.assertFalse(self.store.exists('temp'))

class TestModulos(unittest.TestCase):
    def test_estructura_proyecto(self):
        """Verificar que existen los módulos esperados"""
        modulos = ['main.py']
        for modulo in modulos:
            self.assertTrue(
                os.path.exists(modulo),
                f'Falta {modulo}'
            )

if __name__ == '__main__':
    unittest.main()

Probar en Dispositivo con ADB

# Ver archivos de datos de la app
adb shell ls /data/data/com.ejemplo.miapp/files/

# Extraer JsonStore para inspección
adb pull /data/data/com.ejemplo.miapp/files/data.json

# Ver logs con niveles
adb logcat *:W  # Solo warnings y errores

# Perfilar rendimiento
adb shell am profile start com.ejemplo.miapp /sdcard/profile.trace
# Usar la app
adb shell am profile stop com.ejemplo.miapp
adb pull /sdcard/profile.trace

# Ver threads de la app
adb shell ps -T | grep miapp

Resumen