Almacenamiento específico de la app: Almacena archivos diseñados solo para tu app, ya sea en directorios dedicados dentro de un volumen de almacenamiento interno o en directorios dedicados diferentes dentro del almacenamiento externo. Usa los directorios del almacenamiento interno para guardar información sensible a la que otras apps no deberían acceder. El almacenamiento externo es cuando quieres hacer un archivo que el usuario después puede usar de forma independiente a tu programa.
Almacenamiento compartido: Almacena archivos que tu app pretenda compartir con otras apps, incluidos archivos multimedia, documentos y otros.
Preferencias: Almacena datos primitivos y privados en pares clave-valor. Muy útil para guardar datos sobre la configuración, pero se puede usar para otras cosas también.
Bases de datos: Almacena datos estructurados en una base de datos privada.
Para mayor explicación y una tabla comparativa de todos estos tipos, les sugiero que visiten esta liga. En este post, voy a explicar como usar la primera opción, y de forma más particular, solo el almacenamiento interno.
Según Google (que se puede ver en esta liga), nos dice que son seguros porque en la ubicación donde los guarda en tu teléfono, como está encriptado, no se pueden accesar de otras apps. También hay que considerar que cuando el usuario desinstala tu app, estos archivos se borran (generalmente eso es lo que quieres para que no termines como con Windows que al desinstalar algo deja como mil archivos en tu compu).
Para ejemplificar como se puede almacenar y leer algo del almacenamiento interno, te lo muestro en este video donde explico cómo hice una app que guarda datos de un arreglo de objetos a un archivo, luego los lee. Abajo comparto el código de la app.
En este primer video muestro la interfaz de la app y se ve como funciona:
Y en éste video muestro cómo funciona la app, el código en Java que hace que todo funcione.
Éste es el archivo XML de la interfaz de la app. Yo lo hice con las herramientas visuales, pero aquí se pueden ver todas las propiedades que le asigné a cada control:
packagecom.tony.example.almacena;importandroidx.appcompat.app.AlertDialog;importandroidx.appcompat.app.AppCompatActivity;importandroid.os.Bundle;importandroid.view.View;importandroid.widget.ArrayAdapter;importandroid.widget.EditText;importandroid.widget.Spinner;importandroid.widget.Toast;importjava.io.File;importjava.io.FileInputStream;importjava.io.FileOutputStream;importjava.io.ObjectInputStream;importjava.io.ObjectOutputStream;importjava.util.ArrayList;importjava.util.List;publicclassMainActivityextendsAppCompatActivity{// Declaro un ArrayList de objetos Datos (mismos que voy a guardar en un archivo y// al leer del archivo, los voy a almacenar aquí)List<Datos>listaDatos=newArrayList<>();// Lista para el Spinner de nombresList<String>listaNombres=newArrayList<>();@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}// Método que guarda un nuevo objeto en el ArrayListpublicvoidGuardaEnLista(Viewv){// Creo objetos Java y los vinculo con el layoutEditTexttxtNombre=findViewById(R.id.txtNombre);EditTexttxtLugar=findViewById(R.id.txtLugar);EditTexttxtEdad=findViewById(R.id.txtEdad);SpinnerlstNombres=findViewById(R.id.lstNombres);// Agrego un elemento a mi ArrayList// (creo un nuevo objeto Datos y lo agrego)listaDatos.add(newDatos(txtNombre.getText().toString(),txtLugar.getText().toString(),Integer.parseInt(txtEdad.getText().toString())));// Muestro mensajeToast.makeText(this,"Agregado a la lista",Toast.LENGTH_SHORT).show();// Agrego nombre al spinnerlistaNombres.add(txtNombre.getText().toString());ArrayAdapter<String>llenaSpinner=newArrayAdapter<>(this,android.R.layout.simple_spinner_item,listaNombres);llenaSpinner.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);lstNombres.setAdapter(llenaSpinner);// Limpio los campostxtNombre.setText("");txtLugar.setText("");txtEdad.setText("");}// Método que guarda la lista a un archivo internopublicvoidGuardaEnArchivo(Viewv){// El objeto File me permite acceder el archivo (en este caso, para escribir en él)// (obtengo la ruta donde almacenarlo; en la carpeta de la app)Fileruta=getApplicationContext().getFilesDir();// Éste es el nombre del archivoStringnombreArch="archivo.tpo";// El acceso a archivo tiene que ir en un try catch por si sucede algo inesperadotry{FileOutputStreamescribirArch=newFileOutputStream(newFile(ruta,nombreArch));// Tengo que usar un ObjectOutputStream porque el almacenamiento interno es un archivo de bytes// y este objeto es el que me permite convertir de objeto a byte. Si fuera un String u otra cosa,// bastaría escribirArch.write(lacadena.getBytes())// suponiendo que lacadena es un String que contiene el texto a guardar.ObjectOutputStreamstreamArch=newObjectOutputStream(escribirArch);streamArch.writeObject(listaDatos);streamArch.close();}catch(Exceptione){e.printStackTrace();// Si hay error, que muestre datos sobre el fallo}}// Método que lee del archivo y lo pone en la listapublicvoidLeeDelArchivo(Viewv){// Obtengo referencia a los controles de la pantalla que voy a usarSpinnerlstNombres=findViewById(R.id.lstNombres);// El objeto File con la ruta donde almacenarloFileruta=getApplicationContext().getFilesDir();// Éste es el nombre del archivoStringnombreArch="archivo.tpo";// Borro la lista y borro lo que está en el spinner (el adapter será el arreglo vacío)listaNombres.clear();ArrayAdapter<String>llenaSpinner=newArrayAdapter<>(this,android.R.layout.simple_spinner_item,listaNombres);lstNombres.setAdapter(llenaSpinner);// Leo los datos del archivotry{// FileInputStream me permite abri el archivo para leer de élFileInputStreamleeArch=newFileInputStream(newFile(ruta,nombreArch));// El ObjectInputStream me pemite traducir el arreglo de bytes al ArraylistObjectInputStreamstreamArch=newObjectInputStream(leeArch);// Leo todo y lleno la listalistaDatos=(ArrayList<Datos>)streamArch.readObject();// Cierro el streamstreamArch.close();// Lleno la lista de nombres (strings) con los nombres de la lista de datoslistaNombres.clear();for(inti=0;i<listaDatos.size();i++){listaNombres.add(listaDatos.get(i).getNombre());}// Lleno el Spinner de la nueva listallenaSpinner=newArrayAdapter<>(this,android.R.layout.simple_spinner_item,listaNombres);llenaSpinner.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);lstNombres.setAdapter(llenaSpinner);}catch(Exceptione){e.printStackTrace();// Si hay error, que muestre datos sobre el fallo}}// Método que busca en la lista de datos y muestra información del que// se seleccionó en el SpinnerpublicvoidMuestraDatos(Viewv){// Obtengo referencia a los controles de la pantalla que voy a usarSpinnerlstNombres=findViewById(R.id.lstNombres);// Creo un Alert Builder (ventana estándar que puedo usar)AlertDialog.Builderconstructor=newAlertDialog.Builder(this);constructor.setTitle("Almacena");// Pongo título a la ventanaconstructor.setPositiveButton("Aceptar",null);// Agrego un botón// Si se seleccionó algo en el spinner, le sigolongindex=lstNombres.getSelectedItemId();if(index>-1){// Pongo mensaje a la ventana que voy a mostrarconstructor.setMessage("Nombre: "+listaDatos.get((int)index).getNombre()+" - Lugar: "+listaDatos.get((int)index).getLugar()+" - Edad: "+listaDatos.get((int)index).getEdad());// El texto en la ventana}else{constructor.setMessage("Debe seleccionar un nombre de la lista");}// Creo y muestro la ventanaAlertDialogventanaMensaje=constructor.create();ventanaMensaje.show();}}
Finalmente está el código de la clase Datos, que es con la que se llena el ArrayList y se escribe al archivo:
packagecom.tony.example.almacena;importjava.io.Serializable;// Clase con los datos que voy a almacenar y leer// Serializable es porque debo poderlo mandar a un flujo (stream) para guardarlo en el archivopublicclassDatosimplementsSerializable{// Campos: información a guardarprivateStringnombre,lugar;privateintedad;// Constructor para asignar datosDatos(Stringnombre,Stringlugar,intedad){this.nombre=nombre;this.edad=edad;this.lugar=lugar;}// Métodos para leer datos (getters)StringgetNombre(){returnnombre;}StringgetLugar(){returnlugar;}intgetEdad(){returnedad;}}
Para quienes no somos diseñadores, nos cuesta un poco de trabajo crear recursos para divertirnos haciendo videojuegos. Pero algo padre de la era de internet es que hay muchos sitios donde podemos descargar de forma legal assets (imágenes, sonidos, etc) de forma gratuita, aunque muchos de ellos también hay recursos por los que se pagan.
A continuación escribo una lista de sitios de este tipo que te pueden ser de utilidad.
Unity Asset Store. Éste solo funciona para Unity, pero tiene muchísimas cosas. La liga es para recursos gratuitos, pero hay también de paga que están increíbles.
OpenGameArt. Aquí puedes encontrar un archivo enorme de recursos gratuitos.
Kenney. Aquí tiene muchos recursos gratuitos y unos paquetes de paga.
Itch.io. Es un sitio para compartir juegos (puedes subir aquí tu juego cuando lo termines para compartirlo con la comunidad), pero también tiene una sección de recursos que puedes usar.
GameArt2D. Este sitio tiene muchos recursos en 2D (como es obfvio suponer por el nombre del sitio) que puedes usar.
CraftPix. Es un sitio lleno de assets gratuitos y de pago.
Comunidad r/gameassets en Reddit. Si eres usuario de esta red social, esta comunidad publica recursos gratuitos que le gustan a los miembros.