Интро

Google Play market предлагает сотни вариантов реализации практически любой идеи. Но хорошо продаются только приложения, создающие ощущение, будто они написаны именно для тебя. Сохранение настроек и любой другой пользовательской информации — главное качество user-friendly приложения.

Разработчики SDK предоставили богатый выбор способов для хранения изменяемых данных. Нам нужно только выбрать самый удобный и экономичный (не забываем про производительность). А теперь подробно рассмотрим, какие инструменты есть в мире Android!

Рис. 1. Наглядное отображение способов хранения данных в Android
Рис. 1. Наглядное отображение способов хранения данных в Android

Параллельно с изучением любого обучающего материала рекомендую пролистать официальное руководство. Android развивается очень быстро, и примеры, приведенные на сторонних ресурсах, уже могли устареть.
 

Shared Preferences

Shared Preferences — самый простой и популярный способ хранения данных. Часто упоминается в нашем журнале и вообще лидер по выдаче поисковых запросов в Гугле :). Подходит для жонглирования информацией, умещающейся в одну переменную. Если нужно запомнить какое-то число, строку или булеву переменную — это твой выбор. Хранение данных реализовано по связи «ключ — значение», что позволяет легко и быстро их читать и модифицировать. Из недостатков — нет возможности хранить сложноструктурированную информацию, а также затруднен поиск по имеющимся данным: чтобы получить значение поля, нужно точно знать ключ, перебор не предусмотрен.

SharedPreferences sp;
sp=ctx.getSharedPreferences("Settings", ctx.MODE_PRIVATE);

// Считываем параметр page из настроек под названием Settings. Если такого параметра нет, вернем значение 20
int pageNumber=sp.getInt("page", 20);

// Создаем новый параметр folderName или переписываем значение уже существующего
sp.edit().putString("folderName", folderName).commit();
 

Internal Storage

Хранение данных непосредственно в памяти устройства — самый безопасный способ уберечь данные от шаловливых рук пользователя. Файл будет доступен только для создавшего его приложения, на нерутованном устройстве доступ к таким данным извне получить не удастся, при удалении приложения будет удален и сам файл. До API версии 17 была возможность предоставить доступ другим программам на чтение и запись, теперь же эта функция считается устаревшей (deprecated), и лучше ее не использовать.

При работе с Internal Storage помни, что со встроенной памятью устройства нужно обращаться аккуратно: лимитирован как объем, так и количество циклов записи на нее. При слишком активном использовании велик риск «убить» аппарат, пользователь спасибо не скажет.

String filename = "private.data";
String string = "my important information";
FileOutputStream myFile = null;
try {
  myFile = openFileOutput(filename, Context.MODE_PRIVATE);
} catch (FileNotFoundException e) {
  e.printStackTrace();
}

// Для хранения во внутренней памяти достаточно имени файла
try {
  myFile.write(string.getBytes());
  myFile.close();
} catch (IOException e) {
  e.printStackTrace();
}

// Прочитаем в строковую переменную сохраненный файл
public void LoadData(View v) throws IOException {
  StringBuffer datax = new StringBuffer("");
  FileInputStream fIn = openFileInput(filename);
  InputStreamReader isr = new InputStreamReader(fIn);
  BufferedReader buffreader = new BufferedReader(isr);
  String readString = buffreader.readLine();
  while (readString != null) {
    datax.append(readString);
    readString = buffreader.readLine();
  }
}
 

External Storage

В качестве внешнего носителя может выступать как извлекаемая флеш-карта, так и встроенная память телефона. Такие данные легко доступны извне — и другим приложениям, и пользователю. Поэтому при обращении к ним нужно быть внимательным, сохраненный файл или даже вся флеш-карта могут быть в определенный момент недоступны. Лучше всего подходит для хранения массивных данных: дампов, фотографий, документов и прочего. Не стоит жестко фиксировать путь к файлам на внутренней или внешней памяти, структура папок может меняться в зависимости от версии операционной системы, и есть риск некорректной работы.

Для доступа к папкам со стандартным содержимым (видео, музыка, изображения и так далее) существует класс android.os.Environment, который предоставит путь к ним. Вот так будет выглядеть сохранение изображения с возможностью показывать его в стандартной галерее:

// Сперва нужно получить разрешение у ОС на работу с внешним носителем
<uses-permission
  android:name="android.permission.WRITE_EXTERNAL_STORAGE"
/>
File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES + "/" + foldername + "/");
if (!dir.exists()) {
  dir.mkdirs();
}
OutputStream fOut = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss");
String currentDateandTime = sdf.format(new Date());
String filename="foto_" + currentDateandTime + ".jpg";
File file = new File(dir.toString(), filename);
if(file.exists())
  file.delete();
fOut = new FileOutputStream(file);
mergedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fOut);
fOut.flush();
fOut.close();

// Для отображения в галерее требуется оповестить операционную систему о появлении нового файла (созданного или скачанного)
MediaScannerConnection.scanFile(
  getApplicationContext(),
  new String[] { file.getAbsolutePath() },
  null,
  new OnScanCompletedListener() {
    @Override
    public void onScanCompleted(String path, Uri uri) {
    }
  }
);
}

Чтение и запись будут аналогичны использованию внутренней памяти. Отличие только в том, что необходимо указать полный путь к файлу, а также проверить, доступен ли носитель вообще.

 

SQLite Databases

Для работы со структурированными данными в Android есть возможность создать свою базу данных внутри приложения. Естественно, производительность (и функционал) у нее будет ниже, чем у полноценных MySQL или PostgreSQL, но для мобильных приложений вполне достаточно. Этот метод хранения данных спасает от изобретения велосипеда: нет нужды сбрасывать все в файл, а потом писать парсер, все упрощается до SQL-запросов.

Как ты уже догадался, тебе необходимо минимальное знание SQL — без этого сейчас никуда, в жизни обязательно пригодится :). Мы создадим свою базу данных с поставщиком содержимого, а потом получим к ней доступ из стороннего приложения.

// База данных goodsDB, которая состоит из одной таблицы items
private SQLiteDatabase db;
static final String DATABASE_NAME = "goodsDB";
static final String TABLE_NAME = "items";
static final int DATABASE_VERSION = 1;

// Конструируем SQL-запрос для создания базы данных
static final String CREATE_DB_TABLE = " CREATE TABLE " + TABLE_NAME
  + " (id INTEGER PRIMARY KEY AUTOINCREMENT, "
  + " name TEXT NOT NULL);";

// Непосредственно для создания базы данных используется класс SQLiteOpenHelper
private static class DatabaseHelper extends SQLiteOpenHelper {
  DatabaseHelper(Context context) {
    super(context, DATABASE_NAME, null, DATABASE_VERSION);
  }

  // Для его корректной реализации требуется переобъявить методы onCreate и onUpgrade
  @Override
  public void onCreate(SQLiteDatabase db) {
    db.execSQL(CREATE_DB_TABLE);
  }

  // В них реализуем создание нашей базы данных
  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
   onCreate(db);
  }
}

Ввиду жесткой модели безопасности и разграничения доступа созданная база данных по умолчанию доступна только внутри приложения. Для обмена данными между приложениями требуется использовать так называемый поставщик содержимого (Content Provider). С помощью этого инструмента можно предоставить другим приложениям как структурированную информацию из базы данных, так и отдельные файлы, хранящиеся во внутренней памяти. Существуют стандартные поставщики содержимого: телефонная книга, календарь, браузер и другие.

В Android реализован богатый и гибкий функционал для работы с базами данных, в следующих выпусках нашего журнала мы обязательно об этом расскажем. А сегодня мы будем лаконичны, поэтому информацию добавим тоже через класс ContentProvider. Он позволяет создавать, читать, обновлять и удалять записи (так называемый CRUD-подход для работы с данными).

// В манифесте требуется объявить, что у программы есть поставщик содержимого
<provider
  android:name=".DBProvider"
  android:authorities="com.pahomov.DBgoods.DBProvider"
  android:exported="true"
  android:multiprocess="true" >
</provider>

// Создаем класс DBProvider, в котором реализуем требуемые методы: создание базы данных, чтение и добавление данных
public class DBProvider extends ContentProvider {
  static final String PROVIDER_NAME = "com.pahomov.DBgoods.DBProvider";
  static final String URL = "content://" + PROVIDER_NAME;

  // Создаем статическую ссылку к нашей базе данных
  static final Uri CONTENT_URI = Uri.parse(URL);

  // Объявляем адрес поставщика содержимого для нашей базы данных
  @Override
  public Uri insert(Uri uri, ContentValues values) {
    long rowID = db.insert(TABLE_NAME, "", values);
    if (rowID > 0) {
      Uri _uri = ContentUris.withAppendedId(CONTENT_URI, rowID);
      getContext().getContentResolver().notifyChange(_uri, null);
      // Добавляем новую запись в таблицу через реализованный интерфейс
      return _uri;
    }
    throw new SQLException("Failed to add a record into " + uri);
  }

  @Override
  public boolean onCreate() {
    ontext context = getContext();
    DatabaseHelper dbHelper = new DatabaseHelper(context);
    db = dbHelper.getWritableDatabase();
    if (db != null)
      return true;
    return false;
  }

  @Override
  public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
    SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
    qb.setTables(TABLE_NAME);

    // Выдаем запрошенные данные
    Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
    c.setNotificationUri(getContext().getContentResolver(), uri);
    return c;
  }
}

В стороннем приложении требуется реализовать класс СursorLoader, который позволяет по указанной ссылке асинхронно прочитать данные и получить их в виде объекта Cursor.

@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
  // Создаем новый объект, который будет подгружать данные из нашей базы
  String id;
  String item;
  cursorLoader= new CursorLoader(this, Uri.parse("content://com.pahomov.DBgoods.DBProvider"), null, null, null, null);
  return cursorLoader;
}
@Override
public void onLoadFinished(Loader<Cursor> arg0, Cursor cursor) {
  cursor.moveToFirst();
  // В ответе на SELECT-запрос может быть несколько строк
  while (!cursor.isAfterLast()) {
    id=cursor.getColumnIndex("id");
    item=cursor.getString(cursor.getColumnIndex("name");
    cursor.moveToNext();
  }
}
Рис. 2. Пример работы поставщика содержимого
Рис. 2. Пример работы поставщика содержимого
 

Network Connection

Пользовательские данные хранятся на сервере во всех «взрослых» приложениях: юзеру это добавляет комфорта, он может входить в один свой аккаунт сразу на нескольких устройствах, а заинтересованным личностям такой подход упрощает анализ пользовательских предпочтений и другие маркетинговые операции ;). Загрузить информацию с сервера в интернете можно с помощью стандартных протоколов HTTP и FTP. Мы уже не раз передавали данные через сеть, рекомендую тебе пролистать предыдущие статьи.


Как показывает статистика, в мире еще достаточно пользователей с Android версии 3 и ниже. Они могут стать твоими покупателями, поэтому имеет смысл максимально снизить параметр minSdkVersion.
 

Cache files

Объем ресурсов, выделяемых для каждого приложения, жестко лимитирован, поэтому рано или поздно потребуется сбросить на диск какие-либо временные данные. Для этого существует механизм кеширования, и он удобно реализован в Android. Временные файлы можно хранить как во внутренней (Internal cache), так и во внешней памяти (External cache).

При использовании внутренней памяти нужно помнить, что при нехватке свободного места на устройстве операционная система в первую очередь будет удалять временные файлы. Сам файл создается методом createTempFile(). В качестве аргументов он принимает имя файла, расширение (по желанию) и путь к папке. Папку для таких файлов лучше создавать автоматически методами getCacheDir() и getExternalCacheDir(). При удалении приложения все временные файлы будут также в обязательном порядке удалены (в том числе и с внешнего носителя), в этом их главное отличие от обычных файлов.

File intTmp = File.createTempFile("internal-cache", null, context.getCacheDir());
File extTmp = File.createTempFile("external-cache", ".data",context.getExternalCacheDir());
 

Заключение

Сегодня мы увидели, что у Android-разработчика существует множество вариантов для работы с пользовательскими данными. Теперь у тебя точно нет повода заставлять пользователя вводить логин или лезть в настройки при каждом запуске программы. Как всегда, если есть вопросы — готов ответить через почту :). Удачи!

Оставить мнение