← Volver al listado de tecnologías
Arquitectura y Buenas Prácticas
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
- Eliminar prints de debug
- Configurar log level a warning/error
- Optimizar tamaño de imágenes
- Firmar APK con keystore propio
- Probar en múltiples dispositivos
- Verificar permisos mínimos necesarios
- Implementar manejo de errores
- Agregar analytics (opcional)
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
- Separar código en módulos: screens, widgets, services
- Usar propiedades Kivy para estado reactivo
- JsonStore para persistencia simple
- Threading + @mainthread para operaciones largas
- Logger de Kivy para debugging
- Testing con unittest o pytest