Проблемы, возникающие при разработке android-приложений

    Введение


    Доброго времени суток. В этой статье вы узнаете о том, какие проблемы могут возникнуть при разработке 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. С помощью него очень легко сохранять данные. Рассмотрим на примере сохранение времени.

    SaveObjectRating
    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). На первый взгляд должно работать, но не работает. Мне до конца так и не удалось разобраться почему это работает не так как надо. (оно работает, но с ошибками...)

    Итак, я попробовал ещё один вариант — проверять какой фрагмент виден на экране. Работает так как надо, если использовать его вместе с статической переменной.

    MainActivity.class
    @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

      +3
      Как вариант сойдёт, но если фрагментов будет очень много, то создаются огромные выборки статической переменной и начитаешь путаться, какое значение отвечает за какой фрагмент.

      Откройте для себя правило: никаких магический чисел в коде. И проблема уйдет сама собой.
        0
        Проблема 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+java
          –3
          Проблема очень эстетично решается через создание синглтона главной активити, в которой происходит управление фрагментами, и уже через синглтон вызывать открытие нового фрагмента

          Именно это я и описывал дальше.
          Ещё один вариант, предлагаемый документацией Google — это через объект интерфейса, то есть фрагмент обрабатывает нажатие на свои кнопки и при нажатии создаётся объект интерфейса, который реализует активность
          … Умейте дочитать до конца.
            +1
            Просто вы завуалировали ответ, судя по которому может несколько решений в голову прийти. Я же конкретизировал метод который эстетично подходит для решения данной проблемы, и является общепринятым.
            Просто вы пишите статью о том с какими проблемами столкнулись и какие решения приняли, и писать это в общих чертах не есть правильно, немного конкретики не помешает.
            Вообще многие описанные проблемы не считая проблемы №6 очень банальны, и если вы пришли в андроид с любого другого языка программирования, то многие эти проблемы должны отпасть сами собой, потому что принципы одни и те же, можно легко на гуглить решение для конкретного языка/платформы
              0
              К слову дочитал статью до конца, некоторые банальные моменты даже перечитал
          0
          А что вы отображаете во фрагментах можно узнать? Просто судя по вашему предыдущему посту, у вас приложения на 3-4 активити без какой-то сложной графики. Не могу понять, что там может тормозить.
          System.out.println(«MainActivity:onBackPressed()»);

          Ну не так же логи выводятся в андроиде то) Вот, почитайте.
          if (idTheCurrentFragment == 4) {

          Используйте константы же. В этих цифрах запутаться легче легкого.

          А вообще, большая часть проблем решилась бы сама собой, если бы вы просто ушли от фрагментов, от них реально получить пользу можно только в очень редких случаях, а в основном одни проблемы.
            –1
            приложения на 3-4 активити без какой-то сложной графики. Не могу понять, что там может тормозить.

            Тут наверное дело не в мощности, а в версии андроид. На телефоне под андроид 4.1 видно как создаётся ещё одно окно. И ещё я сравнивал по ОЗУ. Фрагменты экономнее.
              0
              И ещё я сравнивал по ОЗУ. Фрагменты экономнее.

              Исключительно на момент старта. Если архитектура правильная то разницы вы абсолютно никакой не увидите. К примеру переделывал свой, довольно большой проект (более 30 уникальных активити) на фрагменты, переделывал исключительно из-за навигации. Так вот по производительности особую разницу я не заметил. Если у вас ощутимо тормозит запуск новой активности, то стоит обратить внимание где именно происходит утечка, видимо что то в основном потоке нагрузочное стартует
                0
                К слову, фрагмент на то и называется фрагментом, что он заменяет часть GUI, за новое окно отвечает именно Activity. Нужно понимать разницу и разделять эти понятия, каждый метод предназначен для своей задачи
                +1
                На телефоне под андроид 4.1 видно как создаётся ещё одно окно.

                До андроида 3.0 вообще жили на одних активити, и ничего, делали плавные приложения)
                И ещё я сравнивал по ОЗУ. Фрагменты экономнее.

                Пользователи себе телефоны с 3+гб оперативы покупают, чтобы мы им по 5-10мб экономили чтоли?)

                Можно увидеть Методы onCreate и onStart одного из ваших фрагментов? Просто интересно что вы там загружаете.
                К примеру, если вы там каждый раз дергаете SharedPref подобным образом
                sharedPref = context.getSharedPreferences(«RatFile», MODE_PRIVATE);

                это будет давать лаг.
                SharedPref лучше вообще инициализировать один раз при запуске, через контекст приложения, а потом обращаться к уже созданному объекту.
                  0
                  Я думаю SharedPref тут меньше из зол, он даже не даст настолько ощутимого эффекта о котором говорит автор, там что-то по серьезднее запускается. Еще подобный косяк кстати был замечен на сдк >=20, там в анимации активити при переходах иногда бывают затупы, ощущение что они искуственные чтобы перевести все на фрагменты
                    0
                    Ну а чо, когда 3 андроид вышел, все и так кинулись использовать эти фрагменты. Казалось, что делать 1 активити и 150 фрагментов в нем, это спасение. Потом правда попустило)
                      0
                      Если мне память не изменяет, то 3 андроид только для планшетов был. И там гугл сам чуть ли не настаивал на использовании фрагментов. Просто там дизайн в приложениях был таков что менялись части активити, а не вся активити. То есть фрагменты изначально и были созданы для того что бы изменить часть активити не прибегая к полной ее перезагрузки (заменить ее фрагмент), а так же фрагменты были и есть предназначены для повторного использования кода. Например вам нужно показать один и тот же участок с логиков в разных окнах, тут на помощь как раз и приходят фрагменты. Или что бы в 1 активити не плодить тонну заменяемых объектов, которые могут замедлить работу приложения. Но фрагменты никак не панацея. Нужно уметь разделять понятие фрагментов и активити. Активити это окно грубо говоря, а фрагмент — это фрагмент этого окна.

                      Мне данный момент в ios нравится, там на подобии фрагментов есть ContainerView, который используют в главном окне что бы разделить какую то логику в окнах. Но там это реализовано так, что разработчик не хочет использовать это повсеместно, а лишь использует при необходимости и только там где это реально нужно

                      Увы такая мода если новое что то выходит это используют повсеместно, потом начинают осознавать что это не есть хорошо
                    –1
                    Объект рейтинга я создаю только один раз. Лагов ни каких нету. Просто анимация создания активити очень некрасива. Я от неё избавился через фрагменты.
                      +1
                      Изменили бы ее, хотя бы так для начала)
                      ....
                      startActivity(intent); //стартуем активити
                      overridePendingTransition(R.anim.fadein, R.anim.fadeout); //меняем анимацию запуска/закрытия активити
                      
                        –1
                        Проверил ваше решение и нашёл только единственный минус — при каждой транзакции нужно вызывать:
                        overridePendingTransition(R.anim.animation,R.anim.animation);
                        Файл R.anim.animation пустой, чтобы не было никакой анимации.Мне нравиться ваш способ решения проблемы.
                          0
                          Вообще это общепринятый способ решения, который описан в документации
                0
                По поводу 4 проблемы. Я использую конструкцию
                if(currentFragment.getClass().equals(FragmentName.class){...}
                Я в разработке только недавно, может эта конструкция и не есть правильной, но работает)
                  –1
                  Спасибо за ваш комментарий.Мне нравиться ваш способ и в дальнейшем буду его использовать
                    0
                    Так вы же вроде описали что делали сравнение класса и этот метод ведет себя не адекватно
                      0
                      Я использовал ключевое слово instanceof, а не сравнение классов.
                  0
                  Явно что-то делается не так, раз в простом приложении активити тормозит на старте так, что это видно. Фрагменты использовать можно и нужно, но лепить все приложение в одной активити — явно другая крайность. Опять же, если хотите, чтобы у каждого фрагмента был свой идентификатор в стэке — используйте тэги (в качестве тэгов можно использовать статические переменные с понятным названием в каждом фрагменте: 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. Удачи!
                    0
                    попробуйте realm.

                    Если данные типа настроек, или сохранение состояния какой либо переменной, то тут лучше использовать SharedPreferences. Реалм очень большой, если для проекта 6-7 мб лишних не проблема то реалм идеальный вариант, если проект весит меньши библиотеки то реалм зло. Я в своем проекте из-за этого использовал Sugar ORM, она не такая шустрая, но если правильно распределить потоки, то особо не заметно, но принцип работы и синтаксис аналогичен реалму, и вес прибавляется всего пара сотен килобайт. Просто пользователи ругались на размер, а у конкурентов был размер на много ниже, из-за этого пришлось попрощаться с реалмом, и визуально в скорости не проиграл
                      0
                      Тяжело с SQLite...

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

                      N5. По поводу кнопок — кнопки можно создавать и в XML и программно и как вашей душе угодно, а есть еще такая замечательная вещь как data binding…

                      Воу, это же мрак, особенно для новичка. Сколько не общался с людьми, все только и плюются от этой «фичи».
                      Лучше уж рассказать про тот же Butter Knife, который делает примерно тоже самое. Но в разы удобнее и понятнее.
                        0
                        Не так страшен databinding (далее db) как его малюют, если никакую логику в XML не городить. Создать на каждый стандартный для вашего приложения кусок UI по вью и по модели, привязанной через db. Далее в xml фрагмента (или активити) как в конструкторе накидываем созданные вью и фрагмент байндим на ViewLogic класс. В классе фрагмента (или активити) только инициализируем наш VL класс (еще лучше — инжектим Dagger-ом). А во VL выполняем всю логику и настраиваем/меняем модели наших «кусков» как душе угодно. При разработке достаточно большого приложения получаем весьма удобный инструментарий, легко масштабируемый и очень легко покрываемый юнит тестами. (P.S. ButterKnife хорош, его использовали до того, как перешли на db)
                          +1
                          Ну, к MVVM душа лежит не у всех)
                          Все равно выглядит слишком сложно, а все ради чего, чтобы уйти от findbyid, onclicklistener`or да нескольких кастомных атрибутов? Ну не знаю. Реально интересная вещь, это слежение за данными и автоматическое изменение состояния view, но опять же, без данной библиотеки это просто еще 1 строчка в коде. Про KISS тоже забывать не нужно)

                          p.s. Хотя честно говоря, мое знакомство с databinding было уже довольно давно, может они там чего нового придумали, но я увы не в курсе. А все выше сказанное, так, мысли в слух, не более.
                      0
                      Для проблемы 6 можно легко нагуглить решение SVG to XML (SVG2andrid) вот например можно сразу несколько файлов обрабатывать.
                        –1
                        Спасибо за ваш комментарий. Вы облегчили мне жизнь раз в 20.
                        0
                        Проблема 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».
                          –1
                          Для проблемы 6 используйте поддержку VectorDrawable в студии — это генерация картинок в билдтайм для старых андроидов и использование svg практически напрямую для api >=21. https://android-developers.googleblog.com/2016/02/android-support-library-232.html
                            –1
                            Спасибо за ваш комментарий, разобрался.
                            0
                            что касается импорта пачки svg, то можно воспользоваться вот этим плагином, он ковертнет svg в Vector drawable

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое