Разработка под Android / [Из песочницы] Пишем плагины для Android

в 7:28, , рубрики: addons, android, plugins, метки: , ,

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

Введение

Итак, к примеру Вы хотите сделать скачиваемые отдельно игровые уровни или же отдельные темы для своего приложения. Сделать это можно тремя способами:

  • Организовав скачку файлов со своего сервера во внешнее хранилище (SD card)
  • Создав отдельное приложение, которое содержит все необходимые дополнительные ресурсы
  • <a rel="nofollow" href="http://habrahabr.ru/blogs/android_development/123306/">Здесь на хабре описывалось создание модульного приложения, но это немного не то (к слову там же автор упомянул о методе, о котором я Вам расскажу)

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

Реализация

В основном приложении реализуется вся необходимая логика, в приложение-plugin'e — дополнительные ресурсы, а кроме них можно немного переделать главную Activity, которая сообщит пользователю о том, что это всего лишь плагин и направит его в основное приложение. Сделать это можно так:

Intent intent = new Intent(); intent.setClassName("package.name", "package.name.LauncherActivityName"); startActivityForResult(intent,REQUEST_CODE); 

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

ArrayList<String> packs = new ArrayList<String>(); PackageManager mngr = context.getPackageManager(); List<PackageInfo> list = mngr.getInstalledPackages(0); 	for (PackageInfo packageInfo : list) { 		if (packageInfo.packageName.indexOf(context.getPackageName()) != -1 && !packageInfo.packageName.equals(DONATE_PACK) && !packageInfo.packageName.equals(FREE_PACK)) { 			packs.add(packageInfo.packageName); 

Возможно немного избыточный кусок кода. Кроме простой проверки на соответствие имени пакета нашему имени здесь отметаются 2 приложения: FREE & DONATE версии моего приложения. Зная имена пакетов мы можем получить доступ к ресурсам приложений:

PackageManager mngr = getPackageManager(); res = mngr.getResourcesForApplication(pack); if (res != null) {// use it!} 

Подводный камень номер раз

Когда я реализовывал это в первый раз, то было попытался просто получить доступ к нужному ресурсу через R.string, R.id, etc. Но естественно (теперь уже — естественно) нумерация в R файле других apk — другая, и для начала мы должны получить id нужного нам ресурса с помощью:

int id = res.getIdentifier("app_name", "string", pack); String name = res.getString(id); 

В примере выше мы получили id, а дальше прочитали полученный ресурс из уже имеющегося у нас объекта типа Resources. Считывание strings применяется у меня если есть необходимость предоставить пользователю листинг имеющихся плагинов. В таком случае мы должны предоставить ему локализованные названия, добавив их в R.strings (см. любой экзампл «Пишем первое приложение..»).
При этом если мы хотим считывать layout'ы, то мы так же можем называть их фиксированными именами, после чего аналогичным образом получить из ресурсов, но тут мы натыкаемся на…

Подводный камень номер два

После того как мы сделаем inflate нашему layout'у, везде, где в xml используются ссылки на ресурсы, будут использоваться ресурсы из нашего главного приложения. Т.е. если в layout'e из плагина на кнопке стоял некий background="@drawable/best_bg", то после прочтения layout'a на фон элементу пойдет вовсе не то, что лежит в ресурсах плагина, а нечто из основного проекта с таким же id, если оно вообще будет найдено, иначе — креш. Избегаем этого так:

drawID = res.getIdentifier(layoutName + "_btn", "drawable", plugins[i]); Bitmap bmp = BitmapFactory.decodeResource(res, drawID); NinePatch patch = new NinePatch(bmp, bmp.getNinePatchChunk(), null); NinePatchDrawable drawable = new NinePatchDrawable(patch); btn.setBackground(drawable); 

Итого: мы получаем ресурсы ручками из ресурсов плагина и назначаем их на полученные (так же ручками) вьюшки. При этом для того, чтобы получить экземпляры элементов, лежащих в этом layout'e я использую конструкцию:

view.findViewWithTag(tag); 

Все по той же причине — мы не можем обратиться к ней через R.id.
Ну и напоследок.

Подводный камень номер три

Как Вы видели выше — я читаю NinePatch ресурсы, но если вы просто добавите эти ninepatch.9.png в проект-плагин, то сильно удивитесь — изображения будут растянуты как обыкновенные изображения. Суть в том, что adb компилирует ninepatch'и — только после этого мы можем их использовать. И по видимому когда мы обращаемся к drawable в плагине нам выдаются исходные не откомпилированные ресурсы. Починить это можно, рецепт такой:

  1. Компилируем проект плагина
  2. Разархивируем его
  3. Достаем ninepatch'и и копируем с заменой в проект
  4. Снова компилируем, профит

Заключение

Прошу строго не судить — это мой первый пост на хабре и я не со всем разобрался, например — как сделать отступы в коде и почему несмотря на закрытый тег кода текст статьи начиная с некоторой строки имеет стиль, которым оформляется код. Надеюсь изложенное будет кому-то полезно. Применяю этот метод успешно на всех платформах 1.6+, ничего не глючит и не падает.
Единственный минус этого подхода — лежащие как отдельные приложения плагины. Но даже в android market'e есть отдельная категория для библиотек (Разное/Библиотеки и демоверсии).
UPD: спасибо пользователю andycaramba за помощь с форматированием.

Автор: JaLoveAst1k

Поделиться

* - обязательные к заполнению поля