Введение
Доброго времени суток. В этой статье вы узнаете о том, какие проблемы могут возникнуть при разработке android-приложений. На написание этой статьи меня побудили комментарии из прошлой статьи, кстати вот она: моя первая статья. Спасибо за советы! Ну, пора начинать…
Проблема 1
Разрабатывая свои приложения мне хотелось сделать их как можно удобнее и красивее. Этому мешали стандартные диалоговые окна. Мне хотелось, чтобы фон в диалоге совпадал с основным фоном приложения.
Решение
В манифесте нужно выбрать собственную тему, за это отвечает атрибут:
android:theme="@style/AppTheme">
Заходим в values/styles.xml:
В этом файле находится тема приложения. Сначала я сделал приложение во весь экран, через наследование темы — android:Theme.Black.NoTitleBar.Fullscreen, потом изменил стиль диалоговых окон таким образом:
<item name="android:alertDialogStyle">@style/alertStyle</item>
но такой вариант не работает… За стиль диалоговых окон отвечает именно такой атрибут:
<item name="android:alertDialogTheme">@style/alertStyle</item>
Теперь уже можно определить сам стиль в том же файле:
<style name="alertStyle" parent="android:Theme.Dialog">
<item name="android:layout_gravity">center</item>
<item name="android:background">#64abcdf5</item>
</style>
<resources>
<style name="AppTheme" parent="android:Theme.Black.NoTitleBar.Fullscreen">
<item name="android:alertDialogTheme">@style/alertStyle</item>
</style>
<style name="alertStyle" parent="android:Theme.Dialog">
<item name="android:layout_gravity">center</item>
<item name="android:background">#64abcdf5</item>
</style>
</resources>
В результате получаем:
Проблема 2
Эта проблема связана с сохранением данных. Сначала я находил решения связанные с созданием баз данных, но это для меня было чем-то страшным. Я искал вариант проще. Наконец набрёл в официальной документации на класс SharedPreferences. С помощью него очень легко сохранять данные. Рассмотрим на примере сохранение времени.
public final class SaveObjectRating {
private Date[] convertedTime;
private SharedPreferences sharedPref;
private long[] time;
SaveObjectRating(Context context) {
time = new long[10];
convertedTime = new Date[10];
sharedPref = context.getSharedPreferences("RatFile", MODE_PRIVATE);
load();
}
public Date[] getConvertedTime() {
return convertedTime;
}
public void setNewTimeOnIndex(long a, int index) {
if (a < time[index] && a != 0) {
time[index] = a;
}
}
//метод сохранения данных
public void save() {
SharedPreferences.Editor editor = sharedPref.edit();
for (int i = 0; i < 10; i++) {
editor.putLong("key" + i, time[i]);
}
editor.apply();
}
//метод загрузки данных из хранилища
private void load() {
for (int i = 0; i < 10; i++) {
time[i] = sharedPref.getLong("key" + i, 3599);
}
}
//Метод преобразования
void createConvertedTime() {
for (int i = 0; i < 10; i++) {
convertedTime[i] = new Date();
long min = time[i] / 60;
long sec = time[i] % 60;
convertedTime[i].setMinutes((int) min);
convertedTime[i].setSeconds((int) sec);
}
}
}
Данный класс используется для сохранения потраченного игроком времени на прохождение уровня. При создании объектов этого класса в них будут уже загруженные данные, потому что в конструкторе класса вызывается метод load().
Новое потраченное время можно задать методом:
setNewTimeOnIndex(long a, int index)
Он изменяет данные только в том случае, если пользователь потратил на прохождение меньше времени.Чтобы сохранить данные нужно вызвать метод save().
И проблема сохранения данных решена без создания баз данных.
Проблема 3
Всё начиналось с много-экранных приложений. Экраны нужно менять, но как. Первое решение которым я долгое время пользовался — это создание для каждого экрана своей активности. Если бы у меня был мощный телефон, как у моего друга, я бы даже не заметил минусов этого решения, но это не так. При переходе на другой экран создавалась ещё одна активность и визуально на слабом телефоне это очень заметно. После осмысления я наконец понял, что активности не самый лучший вариант для переходов между экранами приложения. Нужно было использовать фрагменты. Это даёт нормальный визуальный эффект — совсем не заметно смены экрана.
Проблема 4
Наверное самая сложная проблема, потому что до сих пор каким бы решение я не пользовался приходится ставить куда-нибудь костыль, заключается в том, чтобы реализовать определение текущего фрагмента. Первое, что пришло мне в голову — это использовать статическую переменную. Как вариант сойдёт, но если фрагментов будет очень много, то создаются огромные выборки статической переменной и начитаешь путаться, какое значение отвечает за какой фрагмент. Следующий вариант — это определять на каком мы сейчас фрагменте через его тип (использование ключевого слова языка программирования Java — instanceof). На первый взгляд должно работать, но не работает. Мне до конца так и не удалось разобраться почему это работает не так как надо. (оно работает, но с ошибками...)
Итак, я попробовал ещё один вариант — проверять какой фрагмент виден на экране. Работает так как надо, если использовать его вместе с статической переменной.
@Override
public void onBackPressed() {
System.out.println("MainActivity:onBackPressed()");
if (idTheCurrentFragment == 4) {
finish();
}
if (start.isVisible()) {
System.out.println("Вы нажали назад в главном меню приложения");
idTheCurrentFragment = -1;
finish();
} else if (game1vs1.isVisible() || gameCompany.isVisible()) {
confirmation(); //Метод создаёт диалоговое окно с выбором.
} else if (idTheCurrentFragment == -1) {
System.out.println("Вы нажали кнопку назад");
super.onBackPressed();
}
}
Задача состояла в том, чтобы при нажатии системной кнопки назад игрока спрашивали уверен ли он в том, что хочет покинуть игру. Если игрок соглашается, то вызывается метод экземпляра класса FragmentManager popBackStack("..."), статическая переменная выставлялась в значение 4, при котором при нажатии на кнопку ещё раз приложение завершает работу. Такое решение меня устроило, но было бы интересно как это можно сделать ещё лучше.
Проблема 5
Проблема динамической смены фрагментов. К примеру, есть фрагмент А и при нажатии по кнопке, которая находится в размете этого фрагмента, должен открываться следующий экран, то есть фрагмент B. Я попробовал вариант — в классе активности создал обработчик событий нажатия на все кнопки в приложении. Во всей разметке я для всех кнопок указал в атрибуте android:onClick="..." созданный в активности обработчик событий. Для кнопок обязательно нужно указать id, чтобы обработчик понимал с какой кнопкой он имеет дело.
public void onClick(View v) {
int id = v.getId();
switch (id) {
case R.id.exit: {
onBackPressed();
break;
}
case R.id.info: {
fm.beginTransaction().replace(R.id.fragment_container, infoFragment).addToBackStack(null).commit();
break;
}
case R.id.about_game: {
fm.beginTransaction().replace(R.id.fragment_container, aboutGameFragment).addToBackStack(null).commit();
break;
}
case R.id.about_authors: {
fm.beginTransaction().replace(R.id.fragment_container, aboutAuthorsFragment).addToBackStack(null).commit();
break;
}
}
Сами кнопки не создаются в java коде, только в xml. Да, да это работает. В активности есть экземпляр класса FragmentManager, чтобы выполнить транзакцию, то есть поменять текущий фрагмент.
Всё это хорошо, но я не знаю считается ли это правильным, поэтому от этого варианта я быстро отказался, постепенно убирая из выборки по нескольку значений, пока она совсем не опустела. Ещё один вариант, предлагаемый документацией Google — это через объект интерфейса, то есть фрагмент обрабатывает нажатие на свои кнопки и при нажатии создаётся объект интерфейса, который реализует активность таким образом:
SendingActivity message = (SendingActivity) getActivity();
message.send(''сообщение для активности'');
В реализации активностью этого интерфейса можно сделать switch — case, в котором уже определять какой фрагмент открывать дальше.
@Override
public void send(int a) {
switch (a) {
case 0: {
//Открываем фрагмент первого режима игры
break;
}
case 1: {
//Открываем фрагмент второго режима игры
break;
}
case 2: {
//Открываем фрагмент третьего режима игры
break;
}
case 3: {
//Открываем фрагмент четвёртого режима игр
break;
}
case 4: {
//Закрываем приложение(Кнопка выход)
finish();
break;
}
}
Проблема 6
Это проблема осталась так не решённой. Я надеюсь найдутся какие-нибудь опытные разработчик, которые смогут поделиться своим опытом в комментариях. Дело в том, что часто бывает нужно импортировать свою картинки svg-формата в среду разработки, но делать по одной картинки очень долго, даже если натренироваться… История была такой, я мучился вставлял 80 картинок в среду разработки, а потом оказалось, что их нужно переделывать…
Заключение
Молодцы те, кто дочитал до конца. Итак, я проделал длинный путь обхода этих проблем, чтобы на моих ошибках научились другие и поделился своим опытом с начинающими android-разработчиками. Для примеров я использовал написанных мною приложения. Вот ссылки на них:
play.google.com/store/apps/details?id=com.thekingames.memorize
play.google.com/store/apps/details?id=com.thekingames.warrior
Я надеюсь, что моя статья была для вас полезна. Всем удачи!
Комментарии (32)
trilodi
17.02.2017 20:54Проблема 5
Проблема очень эстетично решается через создание синглтона главной активити, в которой происходит управление фрагментами, и уже через синглтон вызывать открытие нового фрагмента
Проблема 2
Ну тут обычно все наоборот, все обычно начинают с SharedPreferences, а потом уходят в сторону БД, как бы логично от простого к сложному. Просто нужно понимать разницы, SharedPreferences предназначен больше сохранения состояний/настроек, можно конечно его и под все подряд использовать, но это не есть правильно.
Даже если быть самоучкой то первый поиск в гугле приводит к SharedPreferences https://www.google.se/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=%D1%81%D0%BE%D1%85%D1%80%D0%B0%D0%BD%D0%B5%D0%BD%D0%B8%D0%B5+%D0%B2+android+javaureneva1999
18.02.2017 16:49-3Проблема очень эстетично решается через создание синглтона главной активити, в которой происходит управление фрагментами, и уже через синглтон вызывать открытие нового фрагмента
Именно это я и описывал дальше.
Ещё один вариант, предлагаемый документацией Google — это через объект интерфейса, то есть фрагмент обрабатывает нажатие на свои кнопки и при нажатии создаётся объект интерфейса, который реализует активность
… Умейте дочитать до конца.trilodi
18.02.2017 18:10+1Просто вы завуалировали ответ, судя по которому может несколько решений в голову прийти. Я же конкретизировал метод который эстетично подходит для решения данной проблемы, и является общепринятым.
Просто вы пишите статью о том с какими проблемами столкнулись и какие решения приняли, и писать это в общих чертах не есть правильно, немного конкретики не помешает.
Вообще многие описанные проблемы не считая проблемы №6 очень банальны, и если вы пришли в андроид с любого другого языка программирования, то многие эти проблемы должны отпасть сами собой, потому что принципы одни и те же, можно легко на гуглить решение для конкретного языка/платформы
shakagamii
17.02.2017 21:30А что вы отображаете во фрагментах можно узнать? Просто судя по вашему предыдущему посту, у вас приложения на 3-4 активити без какой-то сложной графики. Не могу понять, что там может тормозить.
System.out.println(«MainActivity:onBackPressed()»);
Ну не так же логи выводятся в андроиде то) Вот, почитайте.
if (idTheCurrentFragment == 4) {
Используйте константы же. В этих цифрах запутаться легче легкого.
А вообще, большая часть проблем решилась бы сама собой, если бы вы просто ушли от фрагментов, от них реально получить пользу можно только в очень редких случаях, а в основном одни проблемы.ureneva1999
17.02.2017 21:36-1приложения на 3-4 активити без какой-то сложной графики. Не могу понять, что там может тормозить.
Тут наверное дело не в мощности, а в версии андроид. На телефоне под андроид 4.1 видно как создаётся ещё одно окно. И ещё я сравнивал по ОЗУ. Фрагменты экономнее.trilodi
17.02.2017 21:54И ещё я сравнивал по ОЗУ. Фрагменты экономнее.
Исключительно на момент старта. Если архитектура правильная то разницы вы абсолютно никакой не увидите. К примеру переделывал свой, довольно большой проект (более 30 уникальных активити) на фрагменты, переделывал исключительно из-за навигации. Так вот по производительности особую разницу я не заметил. Если у вас ощутимо тормозит запуск новой активности, то стоит обратить внимание где именно происходит утечка, видимо что то в основном потоке нагрузочное стартуетtrilodi
17.02.2017 22:04К слову, фрагмент на то и называется фрагментом, что он заменяет часть GUI, за новое окно отвечает именно Activity. Нужно понимать разницу и разделять эти понятия, каждый метод предназначен для своей задачи
shakagamii
17.02.2017 22:38+1На телефоне под андроид 4.1 видно как создаётся ещё одно окно.
До андроида 3.0 вообще жили на одних активити, и ничего, делали плавные приложения)
И ещё я сравнивал по ОЗУ. Фрагменты экономнее.
Пользователи себе телефоны с 3+гб оперативы покупают, чтобы мы им по 5-10мб экономили чтоли?)
Можно увидеть Методы onCreate и onStart одного из ваших фрагментов? Просто интересно что вы там загружаете.
К примеру, если вы там каждый раз дергаете SharedPref подобным образом
sharedPref = context.getSharedPreferences(«RatFile», MODE_PRIVATE);
это будет давать лаг.
SharedPref лучше вообще инициализировать один раз при запуске, через контекст приложения, а потом обращаться к уже созданному объекту.trilodi
18.02.2017 05:55Я думаю SharedPref тут меньше из зол, он даже не даст настолько ощутимого эффекта о котором говорит автор, там что-то по серьезднее запускается. Еще подобный косяк кстати был замечен на сдк >=20, там в анимации активити при переходах иногда бывают затупы, ощущение что они искуственные чтобы перевести все на фрагменты
shakagamii
18.02.2017 18:44Ну а чо, когда 3 андроид вышел, все и так кинулись использовать эти фрагменты. Казалось, что делать 1 активити и 150 фрагментов в нем, это спасение. Потом правда попустило)
trilodi
18.02.2017 18:54Если мне память не изменяет, то 3 андроид только для планшетов был. И там гугл сам чуть ли не настаивал на использовании фрагментов. Просто там дизайн в приложениях был таков что менялись части активити, а не вся активити. То есть фрагменты изначально и были созданы для того что бы изменить часть активити не прибегая к полной ее перезагрузки (заменить ее фрагмент), а так же фрагменты были и есть предназначены для повторного использования кода. Например вам нужно показать один и тот же участок с логиков в разных окнах, тут на помощь как раз и приходят фрагменты. Или что бы в 1 активити не плодить тонну заменяемых объектов, которые могут замедлить работу приложения. Но фрагменты никак не панацея. Нужно уметь разделять понятие фрагментов и активити. Активити это окно грубо говоря, а фрагмент — это фрагмент этого окна.
Мне данный момент в ios нравится, там на подобии фрагментов есть ContainerView, который используют в главном окне что бы разделить какую то логику в окнах. Но там это реализовано так, что разработчик не хочет использовать это повсеместно, а лишь использует при необходимости и только там где это реально нужно
Увы такая мода если новое что то выходит это используют повсеместно, потом начинают осознавать что это не есть хорошо
ureneva1999
18.02.2017 16:33-1Объект рейтинга я создаю только один раз. Лагов ни каких нету. Просто анимация создания активити очень некрасива. Я от неё избавился через фрагменты.
shakagamii
18.02.2017 18:40+1Изменили бы ее, хотя бы так для начала)
.... startActivity(intent); //стартуем активити overridePendingTransition(R.anim.fadein, R.anim.fadeout); //меняем анимацию запуска/закрытия активити
ureneva1999
19.02.2017 16:54-1Проверил ваше решение и нашёл только единственный минус — при каждой транзакции нужно вызывать:
overridePendingTransition(R.anim.animation,R.anim.animation);
Файл R.anim.animation пустой, чтобы не было никакой анимации.Мне нравиться ваш способ решения проблемы.
AlexandrHrybuk
18.02.2017 16:25По поводу 4 проблемы. Я использую конструкцию
if(currentFragment.getClass().equals(FragmentName.class){...}
Я в разработке только недавно, может эта конструкция и не есть правильной, но работает)ureneva1999
18.02.2017 16:31-1Спасибо за ваш комментарий.Мне нравиться ваш способ и в дальнейшем буду его использовать
trilodi
18.02.2017 17:55Так вы же вроде описали что делали сравнение класса и этот метод ведет себя не адекватно
vvgladoun
18.02.2017 16:26Явно что-то делается не так, раз в простом приложении активити тормозит на старте так, что это видно. Фрагменты использовать можно и нужно, но лепить все приложение в одной активити — явно другая крайность. Опять же, если хотите, чтобы у каждого фрагмента был свой идентификатор в стэке — используйте тэги (в качестве тэгов можно использовать статические переменные с понятным названием в каждом фрагменте: addToBackStack(YOUR_FRAGMENT_TAG))
N2. Запихивать все подряд в shared prefernces — не самая лучшая идея. Тяжело с SQLite, структура данных простая и объемы данных небольшие, либо нет большого количества связей таблиц? — попробуйте realm.
N5. По поводу кнопок — кнопки можно создавать и в XML и программно и как вашей душе угодно, а есть еще такая замечательная вещь как data binding… Если надо действительно менять фрагмент в той же активити по кнопке — реализуйте callback в активити. Если это принципиально новое нечто — создавайте интент под новое активити.
N6. Про картинки — если речь идет о добавлении как VectorDrawable, то можно используя third party конвертеры перевести все ваши SVG в XML и потом скопировать при закрытой AS в нужную папку (res\drawable).
Сам весьма «молодой» в мобильной разработке. Из личного опыта могу посоветовать послушать последние google IO, все, что касается технических выступлений + codelabs. Разобраться с архитектурой приложений — MVC и MVVM, и как они могут быть реализованы. Идеальное активити — пустое активити, выносите логику отдельные классы — это поможет и с тестированием и с масштабированием в дальнейшем. Ну и крайне рекомендую разобраться со всеми средствами профилирования и анализа производительности — посмотрите видео из серии performance patterns на официальном канале Google Developers в youtube, пройдите курс от гугла на Udacity — многое откроется по проблеме N3. Курсы из Nano degree на Udacity вообще очень неплохи для начинающих — они записаны самими гугловцами и доступны бесплатно, если вам не нужна красивая бумажка об окончании ND. Удачи!trilodi
18.02.2017 18:03попробуйте realm.
Если данные типа настроек, или сохранение состояния какой либо переменной, то тут лучше использовать SharedPreferences. Реалм очень большой, если для проекта 6-7 мб лишних не проблема то реалм идеальный вариант, если проект весит меньши библиотеки то реалм зло. Я в своем проекте из-за этого использовал Sugar ORM, она не такая шустрая, но если правильно распределить потоки, то особо не заметно, но принцип работы и синтаксис аналогичен реалму, и вес прибавляется всего пара сотен килобайт. Просто пользователи ругались на размер, а у конкурентов был размер на много ниже, из-за этого пришлось попрощаться с реалмом, и визуально в скорости не проиграл
shakagamii
18.02.2017 19:00Тяжело с SQLite...
ORM же для этого придумали. Практически любая библиотека дает примерно такое же удобство в использовании, а разница в скорости для большинства проектов будет не существенна.
N5. По поводу кнопок — кнопки можно создавать и в XML и программно и как вашей душе угодно, а есть еще такая замечательная вещь как data binding…
Воу, это же мрак, особенно для новичка. Сколько не общался с людьми, все только и плюются от этой «фичи».
Лучше уж рассказать про тот же Butter Knife, который делает примерно тоже самое. Но в разы удобнее и понятнее.vvgladoun
18.02.2017 23:58Не так страшен databinding (далее db) как его малюют, если никакую логику в XML не городить. Создать на каждый стандартный для вашего приложения кусок UI по вью и по модели, привязанной через db. Далее в xml фрагмента (или активити) как в конструкторе накидываем созданные вью и фрагмент байндим на ViewLogic класс. В классе фрагмента (или активити) только инициализируем наш VL класс (еще лучше — инжектим Dagger-ом). А во VL выполняем всю логику и настраиваем/меняем модели наших «кусков» как душе угодно. При разработке достаточно большого приложения получаем весьма удобный инструментарий, легко масштабируемый и очень легко покрываемый юнит тестами. (P.S. ButterKnife хорош, его использовали до того, как перешли на db)
shakagamii
19.02.2017 01:20+1Ну, к MVVM душа лежит не у всех)
Все равно выглядит слишком сложно, а все ради чего, чтобы уйти от findbyid, onclicklistener`or да нескольких кастомных атрибутов? Ну не знаю. Реально интересная вещь, это слежение за данными и автоматическое изменение состояния view, но опять же, без данной библиотеки это просто еще 1 строчка в коде. Про KISS тоже забывать не нужно)
p.s. Хотя честно говоря, мое знакомство с databinding было уже довольно давно, может они там чего нового придумали, но я увы не в курсе. А все выше сказанное, так, мысли в слух, не более.
Fast_IQ
18.02.2017 16:26Для проблемы 6 можно легко нагуглить решение SVG to XML (SVG2andrid) вот например можно сразу несколько файлов обрабатывать.
jawaharlalnehru
18.02.2017 16:34Проблема 5:
если оба фрагмента принадлежат одной активити, то создаёте в этой активити такой метод:
public void startFragment(Fragment fragment, boolean addToBackStack) { FragmentManager fragmentManager = getSupportFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); fragmentTransaction.replace(R.id.container, fragment); if(addToBackStack) { fragmentTransaction.addToBackStack("frame"); } else { fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); } fragmentTransaction.commit(); }
потом во фрагменте А создаёте поле вашей активити:
MainActivity activity;
в методе onCreateView фрагмента А инициализируете её:
activity = (MainActivity) getActivity();
а в конце в обработчике нажатия кнопки во фрагменте А:
buttonToFragmentB.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { FragmentB fragment = new FragmentB(); activity.startFragment(fragment, true); } });
вуа, как говорится, ля. Бонусом параметром boolean addToBackStack определяете возможность возврата из фрагмента Б во фрагмент А по нажатию кнопки «Back».
Valle
18.02.2017 19:43-1Для проблемы 6 используйте поддержку VectorDrawable в студии — это генерация картинок в билдтайм для старых андроидов и использование svg практически напрямую для api >=21. https://android-developers.googleblog.com/2016/02/android-support-library-232.html
andreich
22.02.2017 10:52что касается импорта пачки svg, то можно воспользоваться вот этим плагином, он ковертнет svg в Vector drawable
AllexIn
Откройте для себя правило: никаких магический чисел в коде. И проблема уйдет сама собой.