Android – Como testear nuestra base de datos

Hace unas semanas dejábamos lista nuestra aplicación de ejemplo para testear. Ya tenía una funcionalidad mínima y faltaban los tests. Si recordáis, nuestra aplicación se conecta a un API, descarga datos y los almacena en base de datos local.

Hoy vamos a testear nuestra base de datos. Ya veréis como es muy sencillo. Vamos allá!

Antes de testear, empezaré explicando muy rápido esta parte. Las bases de datos locales nos ayudan a persistir datos de tal forma que no sea necesaria una conexión a internet ininterrumpida. En el caso de nuestra aplicación, estamos usando la librería Room de Android, la cual es muy potente, y con pocas líneas podemos persistir nuestros datos.

En la carpeta «db» de nuestro proyecto tenemos 2 archivos.

MovieDb es la clase que gestiona nuestra base de datos. A través de anotaciones indicaremos varios parámetros, por un lado las entidades y por otro la versión de nuestra base de datos. Debemos acordarnos de subir esta versión a medida que se introduzcan cambios en esta.

Además, definimos los DAOs que manejará. Vamos a ver este, el cual se encuentra en MovieDao. Como en nuestra base de datos, debemos marcarlo con una anotación, @Dao, para indicar que se trata de un Dao y nuestra librería Room lo reconozca como tal.

También debemos definir las entidades o tablas de nuestro modelo de datos, para ellos, en la carpeta model del proyecto tenemos una entidad Movie. Veámosla.

Ya veis que es muy sencillo, indicamos con una anotación, @Entity, que se trata de una entidad, e indicamos el nombre de las propiedades que forman parte de la primary key. En este caso solo tenemos una pero pueden ser varias.

Y definimos los campos que queremos tener en nuestra entidad como propiedades.

Por último, para inicializar todo esto y empezar a utilizarlo, en nuestra carpeta di, en AppModule, y con la ayuda de Hilt+Dagger, vamos a inicializar nuestra base de datos.

No quiero entrar en detalle con el tema de Hilt+Dagger, ya que haré un artículo más profundo sobre esto, pero basicamente nos ayudará a instanciar las clases que necesitamos a lo largo de la aplicación, y de este modo usar el principio de inyección de dependencias.

Volviendo a como instanciamos nuestra base de datos, por un lado instanciamos nuestra base de datos, dándole un nombre, indicando la clase MovieDb para indicarle que esta es nuestra base de datos.

Además, le decimos con fallbackToDestructiveMigration que cuando tengamos una nueva versión, elimine la versión actual y cree una nueva. Opción interesante si de momento no nos planteamos un sistema de migración.

También instanciamos los DAOs por si queremos acceder directamente a ellos, pero teniendo acceso a la clase de base de datos no nos haría falta.

Bien, si has llegado hasta aquí, ya hemos dado un repaso rápido a la parte que gestiona la base de datos. Vamos ahora a testear nuestra base de datos.

Lo primero que hay que saber es que los tests de base de datos son instrumentados, ¿esto que quiere decir? que los tests se deben ejecutar o en dispositivo físico o en emulador. Este tipo de tests se deben crear dentro de la carpeta que tiene el namespace y entre paréntesis androidTest.

De todo lo que hemos ido viendo vamos a testear nuestros DAOs, de esta forma podremos comprobar que nuestras consultas devuelven los resultados que se esperan de ellas.

Para poder probar esto, lo que haremos será crear una versión de nuestra base de datos que en lugar almacenar en base de datos local, lo hará en memoria. Además, haremos que las peticiones se hagan en el hilo principal de ejecución, de esta forma nos evitaremos efectos colaterales.

Veamos como será esta base de datos de pruebas:

Para empezar tendremos un objeto (db) de tipo MovieDb, en el cual almacenaremos los datos en memoria, y el cual se inicializará antes de cada test.

Al finalizar cada test cerraremos nuestra base de datos. De esta forma siempre tendremos una base de datos vacía, lo cual hará que los datos de un test no interfieran con los datos de otro.

Por último, también creamos un hilo en el cual ocurrirán todas las ejecuciones del test.

abstract class MovieDbTest {

    @OptIn(DelicateCoroutinesApi::class)
    private val mainThreadSurrogate = newSingleThreadContext("Main Thread")

    private lateinit var _db: MovieDb

    val db: MovieDb
        get() = _db

    @OptIn(ExperimentalCoroutinesApi::class)
    @Before
    fun initDb() {
        Dispatchers.setMain(mainThreadSurrogate)
        _db = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            MovieDb::class.java
        ).build()
    }

    @After
    fun closeDb() {
        _db.close()
        Dispatchers.resetMain()
        mainThreadSurrogate.close()
    }
}

Vamos ahora a testear nuestro DAO, para ello creamos una nueva clase MovieDaoTest que heredará la clase anterior, MovieDbTest. Y que ya será, por fin, nuestro test. Para ello, debemos indicar una anotación, @RunWith, para indicar que será un test.

@RunWith(AndroidJUnit4::class)
class MovieDaoTest: MovieDbTest() {

}

En MovieDao el primer método que tenemos es el de inserción. Comprobemos que se insertan bien los datos.

@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insert(movies: List<Movie>)

Para ello, creamos un método para testear. Lo marcamos con la anotación @Test, e importante, al utilizar coroutines, debemos bloquear la ejecución y enviarla al hilo principal con runBlocking.

@Test
fun when_insert_load_same_data() = runBlocking(Dispatchers.Main) {

}

Para poder insertar, primero definiremos un set de datos. En este caso, un listado de Movie.

val list = listOf(
            Movie(id = "01", imdbId = "imdb01", title = "title_01", originalTitle = "original_title_01",
                originalLang = "en", overview = "overview_01", voteAverage = 4.3f, voteCount = 1000L,
                posterPath = null, releaseDate = "release_date_01", popularity = 5f,
                releaseDateTimestamp = 12132313132L),
            Movie(id = "02", imdbId = "imdb02", title = "title_02", originalTitle = "original_title_02",
                originalLang = "en", overview = "overview_02", voteAverage = 2.8f, voteCount = 900L,
                posterPath = null, releaseDate = "release_date_02", popularity = 4.5f,
                releaseDateTimestamp = 12132313132L),
        )

Seguidamente, ejecutamos el método a testear

db.movieDao().insert(list)

Vale, una vez tenemos en nuestra base de datos en memoria nuestros datos, la forma más sencilla para verificar si se insertó es leyendo de nuevo los datos. Después comprobamos que aparezcan los registros que insertamos.

val loaded = db.movieDao().loadMovies().take(1).toList()[0]
assertThat(loaded.size, CoreMatchers.`is`(2))

Utilizamos el mismo método loadMovies de nuestro DAO, que nos leerá todas nuestras películas. Al devolver este método un objeto tipo Flow, debemos tratarlo con el método take, y convertir a lista para finalmente tener la lista que necesitamos para verificar.

Vamos a crear otro test, esta vez del tirón. En este caso, si volvemos a nuestro archivo MovieDao, y nos fijamos otra vez en el método insert, vemos que tiene una anotación @Insert, en la cual se ha configurado que al insertar un registro con una clave duplicada se haga un Replace. Testeemos si esto se cumple.

@Test
    fun when_insert_repeated_replace_data() = runBlocking(Dispatchers.Main) {

        val list = listOf(
            Movie(id = "02", imdbId = "imdb02", title = "title_02", originalTitle = "original_title_02",
                originalLang = "en", overview = "overview_02", voteAverage = 2.8f, voteCount = 900L,
                posterPath = null, releaseDate = "release_date_02", popularity = 4.5f,
                releaseDateTimestamp = 12132313132L),
        )

        // Método a testear
        db.movieDao().insert(list)

        val loaded01 = db.movieDao().loadMovies().take(1).toList()[0]
        assertThat(loaded01.size, CoreMatchers.`is`(1))

        assertThat(loaded01[0].imdbId, CoreMatchers.`is`("imdb02"))

        val list2 = listOf(Movie(id = "02", imdbId = "imdb02_new", title = "title_02b", originalTitle = "original_title_02b",
            originalLang = "en", overview = "overview_02b", voteAverage = 2.8f, voteCount = 900L,
            posterPath = null, releaseDate = "release_date_02b", popularity = 4.5f,
            releaseDateTimestamp = 12132313132L),)

        db.movieDao().insert(list2)

        val loaded02 = db.movieDao().loadMovies().take(1).toList()[0]
        assertThat(loaded02.size, CoreMatchers.`is`(1))

        assertThat(loaded02[0].imdbId, CoreMatchers.`is`("imdb02_new"))

    }

Empezamos muy parecido, creamos un listado con un solo registro, lo insertamos, leemos y verificamos que se ha insertado un registro. Además, tomamos el primer registro que leemos, lo cual nos dará un objeto Movie, y verificamos el valor de la propiedad imdbId.

assertThat(loaded01[0].imdbId, CoreMatchers.`is`("imdb02"))

Ahora volvemos a insertar otro registro, el cual debe tener el mismo valor para la primary key. Leemos otra vez y comprobamos que seguimos teniendo un único registro.

Además, verificamos el valor de la propiedad imdbId otra vez, el cual ahora debe haber cambiado.

Por último, vamos a testear el método que lee un registro concreto. Para ello, hacemos una nueva función de test.

@Test
    fun when_load_on_movie_get_correct_entry() = runBlocking(Dispatchers.Main) {
        val list = listOf(
            Movie(id = "01", imdbId = "imdb01", title = "title_01", originalTitle = "original_title_01",
                originalLang = "en", overview = "overview_01", voteAverage = 4.3f, voteCount = 1000L,
                posterPath = null, releaseDate = "release_date_01", popularity = 5f,
                releaseDateTimestamp = 12132313132L),
            Movie(id = "02", imdbId = "imdb02", title = "title_02", originalTitle = "original_title_02",
                originalLang = "en", overview = "overview_02", voteAverage = 2.8f, voteCount = 900L,
                posterPath = null, releaseDate = "release_date_02", popularity = 4.5f,
                releaseDateTimestamp = 12132313132L),
        )


        db.movieDao().insert(list)

        val loaded = db.movieDao().loadMovie("02").take(1).toList()[0]
        assertThat(loaded.id, CoreMatchers.`is`("02"))
        assertThat(loaded.imdbId, CoreMatchers.`is`("imdb02"))
    }

En este caso, volvemos a crear un listado con datos y los insertamos. Leemos los datos utilizando el método que estamos testeando, loadMovie, y le decimos que lea el registro cuyo valor sea «02».

Una vez tenemos el objeto, verificamos que los valores de sus propiedades coincidan con lo que se espera.

En este caso concreto se está testeando una consulta muy sencilla, pero imaginad cuando tengáis consultas más complicadas, con varios joins o parámetros en el where. La tranquilidad y seguridad que os pueden dar este tipo de test no tiene precio.

Por último, comentaros que he tenido que añadir y eliminar algunas dependencias del proyecto que tengo en github donde voy poniendo el código. Revisar los cambios para saber cuales son las nuevas dependencias.

También os recuerdo que he grabado una sesión en Youtube hablando de este tema, revisarlo, suscribiros y no olvidéis darle un like si queréis que haga más videos testeando aplicaciones.

Código del repositorio https://github.com/3pies/moviesios

Como crear nuestro primer MVVM en iOS

Empezar a crear una aplicación desde cero siempre es excitante y a la vez abrumador. Demasiadas cosas que hacer en muy poco tiempo. Así que es importante pararse un momento y reflexionar como queremos hacerlo.

Un buen punto de inicio es pensar en una funcionalidad que nos permita definir a pequeña escala como queremos que sea la arquitectura de nuestra app, y a partir de ahí empezar a contruir.

En nuestro caso, una app que lea un listado y después vea un detalle nos ayudará a comprender y modelar nuestra app a pequeña escala.

Voy a utilizar MVVM como arquitectura, ya que nos ayudará a separar la parte vista del resto de la app. En esta arquitectura tenemos 3 partes, la parte View que es la que muestra la interface de usuario, la parte Model que es la que maneja los datos, y por último, la parte ViewModel, que por un lado se encarga de tratar los datos y por otro lado, pasa estos datos a la vista a través de databinding.

Vamos a empezar, voy a crear una vista muy sencilla, con su viewmodel y datos mock que nos permitirá tener un primer punto de arranque con estas 3 capas. Empecemos!!!

Model

Empezaré creando el modelo de datos, en este caso al tratarse de un listado de películas, haré un entidad llamada Movie, la cual será un struct plano y cuyo único cometido será almacenar los datos a mostrar.

Además, debe implementar el protocolo Identifiable, el cual nos ayudará después a la hora de iterar sobre un listado de Movie.

struct Movie: Identifiable {
    var id: String
    var imdbId: String?
    var title: String
    var overView: String
    var posterPath: String?  
}

Además, dado que no vamos a conectarnos ni con un API ni con base de datos local de momento, crearemos un array de tipo Movie con datos mock. Este nos quedará después para ayudarnos a crear las previews de nuestras vistas.

let moviesTest: [Movie] = [
    Movie(id: "1", imdbId: "imdb01", title: "Regreso al Futuro", overView: "Lorem ipsum", posterPath: nil),
    Movie(id: "2", imdbId: "imdb02", title: "Regreso al Futuro II", overView: "Lorem ipsum", posterPath: nil),
    Movie(id: "3", imdbId: "imdb03", title: "Regreso al Futuro III", overView: "Lorem ipsum", posterPath: nil),
    Movie(id: "4", imdbId: "imdb04", title: "Matrix", overView: "Lorem ipsum", posterPath: nil),
    Movie(id: "5", imdbId: "imdb05", title: "Canta", overView: "Lorem ipsum", posterPath: nil),
    Movie(id: "6", imdbId: "imdb06", title: "Scream", overView: "Lorem ipsum", posterPath: nil),
    Movie(id: "7", imdbId: "imdb07", title: "Spiderman No way home", overView: "Lorem ipsum", posterPath: nil),
]

ViewModel

El siguiente paso será crear nuestro ViewModel, es decir, la parte que se encargará de leer datos y enlazarlos a la vista.

Crearemos un archivo llamado HomeViewModel con lo siguiente:

import SwiftUI

class HomeViewModel: ObservableObject {
    
    @Published var movies: [Movie] = []
    
    func loadMovies() {
        self.movies = moviesTest
    }
    
}

Tenemos 3 puntos a destacar. Por un lado, nuestra clase ViewModel implementa ObservableObject, lo cual permitirá que se observe desde la vista.

Por otro lado, tenemos una propiedad marcada con @Published, el cual es un wrapper que nos permitirá enviar datos y que será observado desde la vista. La vista reaccionará a los cambios que se vayan produciendo desde aquí.

Por último, tenemos un método loadMovies que se encarga de lanzar la lectura de datos. En este caso concreto, simplemente estamos enlazando con nuestros datos mock, pero en futuras publicaciones se complicará esta parte, mientras que la vista permanecerá igual.

Ahora que tenemos 2 de 3 partes, vamos a por la última.

View

Esta es sin duda la más sencilla. De momento tendremos un listado que se nutrirá de los datos que vienen del viewModel.

struct HomeView: View {
    
    @ObservedObject var viewModel: HomeViewModel = HomeViewModel()
    
    var body: some View {
        VStack {
            ForEach(viewModel.movies) { movie in
                Text(movie.title)
            }
        }.onAppear {
            viewModel.loadMovies()
        }
    }
}

Para empezar definimos nuestro viewModel y lo marcamos con @ObservedObject, esto es un wrapper que le dirá a la vista que se va a observar este objeto.

Después, tenemos un VStack, en el cual añadimos un ForEach del viewModel.movies, junto con un Text, donde de momento solo mostraremos el título de nuestra película.

Hasta aquí, de por si, no pasará nada, pero gracias al modificador onAppear llamaremos al método que lee las películas en el momento en el que aparece la vista.

Y con esto ya hemos montado una arquitectura MVVM muy básica. Aunque muy básica ya hemos marcado los cimientos y separado los conceptos para construir nuestra aplicación.

Como siempre, os dejo enlace al repositorio de Github donde tengo el código completo. No olvidéis revisar el vídeo de Youtube asociado, suscribiros y dejar un gran like.

https://github.com/3pies/moviesios

Descubre como crear tu app iOS de cero a experto

Hace poco os presentaba una app Android que había creado para un proceso de selección. Podéis ver aquí el código de la aplicación https://github.com/3pies/movies

Además del video presentación que hice en mi canal de Youtube.

Y como no podía ser de otra forma, no podemos dejar a nuestros queridos usuarios de iOS sin su versión. Así que empezaré a crear en iOS una versión muy similar. En este caso utilizando SwiftUI.

Hoy nos tocará preparar nuestra app. Para ello lo primero que haremos será crear un nuevo proyecto, indicamos donde lo almacenaremos, en mi caso marcaré crear repositorio git (os recomiendo que vosotros también lo hagáis).

Como interface SwiftUI y lenguaje Swift. En mi caso no utilizaré Core Data, así que lo dejo sin marcar. Indicamos el Bundle Identifier y ya tendremos el proyecto listo.

Si recordáis, en el proyecto Android, tenía que:

  • Leer datos de un WS
  • Almacenar en base de datos local

Así que aprovecharemos y añadiremos nuestras primeras librerías. Para ello vamos a utilizar Swift Package Manager. Este es un gestor de dependencias similar a Cocoapods, y que ya viene integrado en el IDE XCode.

Para añadir nuevas librerías es muy sencillo, primero localizamos las que queremos añadir. En mi caso, voy a añadir primero Alamofire. Librería utilizada para hacer las peticiones a los WS. Vamos a su github y allí nos aparecen las instrucciones para añadir https://github.com/Alamofire/Alamofire#installation

En XCode, vamos a File -> Add Packages… En la ventana que aparece, arriba a la izquierda, en el buscador escribimos «https://github.com/Alamofire/Alamofire.git» y pulsamos «Add Package». Empezará a cargar hasta que finalmente acabe.

Una vez lista esta podemos añadir alguna más. Os dejo enlaces a los repositorios de:

  • Realm para almacenar de forma local https://github.com/realm/realm-swift.git
  • Resolver para añadir inyección de dependencias https://github.com/hmlongco/Resolver.git

Llegados a este punto, podremos ir a las propiedades de nuestro proyecto, en la sección «Package Dependencies» tendremos todas las dependencias que hemos añadido.

Si en cualquier momento estamos interesados en actualizar nuestras dependencias podemos ir a File -> Packages -> Update to Latest Package versions.

Una vez listas nuestras primeras librerías, ya estaremos preparados para empezar a desarrollar nuestra aplicación.

Por último, os dejo el enlace al repositorio donde voy a hacer la aplicación para iOS https://github.com/3pies/moviesios

Mis Mocks no funcionaban, así es como lo arreglé

Hace poco os compartí un proyecto Android en Github. Se trata de una aplicación para leer películas desde un web service. Aquí os dejo de nuevo el enlace https://github.com/3pies/movies

A partir de ahora empezaré a testearla poco a poco. Hoy empezaremos a preparar nuestra app para testing.

Para testear nuestras clases, lo habitual es crear clases mock que nos ayuden a centrarnos en la clase que estamos testando. Estos mocks se comportarán tal como nosotros queremos para que el resultado del test sea siempre predecible.

Vamos al lío. Lo primero que debemos hacer es añadir las librerías necesarias al proyecto. Para ello, abrimos el archivo build.gradle de nuestra aplicación y añadimos las dependencias necesarias.

Por un lado debemos añadir junit a nuestro proyecto, para tener las funcionalidades básicas de test.

testImplementation 'junit:junit:4.13.2'

Y además la librería Mockito. Fijaros que lo hacemos con «testImplementation», de este modo, las librerías solo se cargarán cuando ejecutemos los test unitarios.

testImplementation "org.mockito:mockito-core:4.6.1"
testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"

Sincronizamos y podemos empezar nuestros tests. Pero, no os vayáis todavía, que queda una sorpresa.

Voy a crear un ejemplo de clase para probar. En el repositorio lo podréis encontrar en el archivo Playground.kt, el cual usaré para diferentes pruebas.

class Bar() {
    fun sayHello(): String { return "hello" }
}

class Foo constructor(private var bar: Bar) {
    fun saySomething(): String {
        return bar.sayHello()
    }
}

Ok, tenemos una clase Foo que usa Bar para devolver un texto. Desde Foo el método saySomething llama al método sayHello de bar. Creemos un test para verificar que esto siempre se cumple.

Vamos a crear nuestro primer test unitario sobre la clase Foo. Para ello, con la vista Android seleccionada, vamos a la carpeta de test. Es la que tiene el nombre de nuestro namespace y entre paréntesis aparece test.

Creamos otro archivo para pruebas, le llamaré PlaygroundTest.kt

Creamos nuestra clase de Test y definimos un mock para Bar. Nuestra clase la marcamos con una anotation que indicará que se ejecuta con Junit, y para nuestro mock hacemos uso de Mockito.mock. Simple.

@RunWith(JUnit4::class)
class FooTest {

    private val bar = Mockito.mock(Bar::class.java)
    
}

Y dentro de esta clase debemos crear nuestro primer test. Veámoslo.

@Test
fun `Test say something`() {
    Mockito.`when`(bar.sayHello()).thenReturn("goodbye")

    val foo = Foo(bar)

    val result = foo.saySomething()

    verify(bar, times(1)).sayHello()
    assertThat(result, CoreMatchers.`is`("goodbye"))
}

Lo primero que hacemos es definir como se comportará el mock de Bar cuando llamemos a «sayHello». Seguimos instanciando un objeto Foo y llamando el método a testear: saySomething. Y por último, verificamos que se ejecuta sayHello y el resultado es el que se espera. Fácil, no? Pues no.

Ejecutad este test y veréis que da error. Aquí es donde quería llegar. El problema es que nuestra clase Bar y el método sayHello necesitan que se declaren como open.

Podéis probar a marcar como open Bar y su método y volver a ejecutar, veréis que ahora se ejecuta correctamente.

open class Bar() {
    open fun sayHello(): String { return "hello" }
}

Ya está, se acabó, no? Pues no.

Si ya es público, ¿porque necesito abrirlo aún más? Deberíamos dar a las clases el nivel de privacidad más restrictivo posible e ir abriendo solo según se necesite. En este caso, marcar como open clase y método no es la solución ideal, ya que a partir de ahora todas nuestras clases y métodos debemos marcarlos así para poder testear. 🙁

Si al menos tuviéramos algo que nos marcase nuestra clase como open solo cuando ejecutamos los tests… Ehhh, que si que existe eso.

Simplemente haced esto, dentro de la carpeta app de nuestro proyecto creamos carpeta resources, y dentro de esta otra carpeta que llamamos mockito-extensions. En esta carpeta creamos un archivo y le llamamos org.mockito.plugins.MockMaker. En este archivo añadimos la siguiente línea:

mock-maker-inline

Ahora eliminamos la instrucción open de nuestra clase Bar, y listo. Probar a ejecutar el test de nuevo. Ahora funciona correctamente, y nuestras clases y métodos podrán tener la visibilidad que le corresponda.

Creedme, esto puede llegar a ser un quebradero de cabeza para quien se inicia a testear una app. Probadlo y me contáis que tal os fue.

iOS – Alamofire en 3 sencillos pasos

Cuando hacemos aplicaciones móviles, existen ciertas cosas que solemos necesitar en la mayoría de los casos. Una de ellas es nutrir nuestra aplicación con datos, en este caso veremos cómo leer datos de nuestra API HTTP.

Para poder leer datos de nuestras APIs lo más común es utilizar una librería que nos de una serie de utilidades ya empaquetadas y listas para utilizar. No queremos reinventar la rueda una y otra vez. Por eso, en este ejercicio, utilizatemos Alamofire, una de las librerías más populares.

Para empezar, necesitamos añadirlo a nuestro Podfile. Y en nuestra terminal ejecutamos un pod install para que se descargue.

También veremos como con el uso de objetos Codable, mapear las respuestas de nuestra API es muy sencillo.

Si no te quieres perder la explicación, haz click en el video. Y no olvides dejar tus comentarios o preguntas. Y si te gusta el vídeo, y quieres más, no olvides darle like y suscribirte.

Podcast Y

Podcast Y es un reproductor de podcast, así de simple. Con esta app podrás buscar tus podcasts favoritos y escucharlos allá donde vayas.

Así de sencilla ha sido la idea detrás de esta aplicación. El principal reto al que me quise enfrentar fue la de crear un reproductor con el que pudiese escuchar mis podcasts favoritos.

Después del reto inicial han ido surgiendo otros, y surgirán otros nuevos. Como la posibilidad de crear mis propias búsquedas personalizadas y añadirlas al dashboard, filtrado por idiomas o recordar los últimos podcasts que he escuchado y retomarlos donde los he dejado.

Poco a poco se irán añadiendo más funcionalidades, como crear playlists y poder compartir, posibilidad de comentar podcasts, etc…

Espero que la disfrutéis, y no olvidéis dar vuestro feedback.

P.D: De momento es un producto en desarrollo, por lo que se distribuye a través de canal Beta de Google Play.

Get it on Google Play

Politica de privacidad aplicaciones móviles

POLÍTICA DE PRIVACIDAD

El presente Política de Privacidad establece los términos en que usa y protege la información que es proporcionada por sus usuarios al momento de utilizar esta aplicación. Esta compañía está comprometida con la seguridad de los datos de sus usuarios. Cuando le pedimos llenar los campos de información personal con la cual usted pueda ser identificado, lo hacemos asegurando que sólo se empleará de acuerdo con los términos de este documento. Sin embargo esta Política de Privacidad puede cambiar con el tiempo o ser actualizada por lo que le recomendamos y enfatizamos revisar continuamente esta página para asegurarse que está de acuerdo con dichos cambios.

Información que es recogida

Nuestra aplicación podrá recoger información personal por ejemplo: Nombre, información de contacto como su dirección de correo electrónica e información demográfica.

Uso de la información recogida

Nuestra aplicación emplea la información con el fin de proporcionar el mejor servicio posible, particularmente para mantener un registro de usuarios, y mejorar nuestros productos y servicios. Es posible que sean enviados correos electrónicos periódicamente a través de nuestro sitio con ofertas especiales, nuevos productos y otra información publicitaria que consideremos relevante para usted o que pueda brindarle algún beneficio, estos correos electrónicos serán enviados a la dirección que usted proporcione y podrán ser cancelados en cualquier momento.

3pies está altamente comprometido para cumplir con el compromiso de mantener su información segura. Usamos los sistemas más avanzados y los actualizamos constantemente para asegurarnos que no exista ningún acceso no autorizado.

 

Enlaces a Terceros

Este aplicación pudiera contener enlaces a sitios web que pudieran ser de su interés. Una vez que usted de clic en estos enlaces y abandone nuestra aplicación, ya no tenemos control sobre al sitio al que es redirigido y por lo tanto no somos responsables de los términos o privacidad ni de la protección de sus datos en esos sitios terceros. Dichos sitios están sujetos a sus propias políticas de privacidad por lo cual es recomendable que los consulte para confirmar que usted está de acuerdo con estas.

Control de su información personal

En cualquier momento usted puede restringir la recopilación o el uso de la información personal que es proporcionada a nuestra aplicación. Cada vez que se le solicite rellenar un formulario, como el de alta de usuario, puede marcar o desmarcar la opción de recibir información por correo electrónico. En caso de que haya marcado la opción de recibir nuestro boletín o publicidad usted puede cancelarla en cualquier momento.

Esta compañía no venderá, cederá ni distribuirá la información personal que es recopilada sin su consentimiento, salvo que sea requerido por un juez con un orden judicial.

Se reserva el derecho de cambiar los términos de la presente Política de Privacidad en cualquier momento.

Lucus Bus

lucus bus

Hacia tiempo que no publicábamos aplicación y este viernes nos enorgullece publicar Lucus Bus. Esta aplicación es un desarrollo independiente de cualquier organismo que ha sido llevado a cabo ante la problemática que teníamos para conocer con exactitud las paradas de autobús urbano que existían en Lugo.

Sigue leyendo