Предисловие
Я не являюсь профессиональным разработчиком с огромным стажем в данной области (и это даже не хобби, а лишь нужда в разработке конкретного приложения), потому данная статья, полагаю, будет полезна новичкам, таким же, как и я был в начале разработке своего приложения. Возможно, кто-то найдет что-то полезное из данной статьи, какие-то кусочки окажутся частью ваших будущих разработок.
Я расскажу вам как написать простенькое ToDo-приложение на Android с тремя активностями (рабочими экранами).
Ссылка на проект на Github будет в конце данной статьи.
Установка и первичная настройка
Для разработки приложения я рассмотрю использование бесплатной IDEIntellij от разработчиков JetBrains - Android Studio, у меня версия 4.1.1.
После успешной установки IDE и запуска нажимаем на самую первую кнопкуStart a new Android Studio Project. Далее появится мастер первичной подготовки проекта:
-
выберем подходящий шаблон, в моем случае это Empty Activity - он самый простой для новичков, так как при первом запуске будет всего 1 XML файл с версткой и один java файл MainActivity.
-
На следующем экране придумываем имя приложению; помните, что package name, после публикации на Google Play изменить нельзя (иначе Google Play посчитает это другим приложением (поправьте меня, если я ошибаюсь). Выбираем язык Java, так как по нему данная статья, а также, по нему больше информации в Интернете, чем по Kotlin.
-
Минимальный SDK выбираем под Android 5.0, так как данного API будет предостаточно для наших задач, заодно мы получим большой охват, в том числе старых устройств: планшеты, смартфоны, встроенные системы.
Далее раскрываем вкладку Project и находим в каталоге
Java><Ваш_Проект> файл
MainActivity.java
, в котором мы будем описывать все
происходящее на главном экране.
Подготовка макетов (layouts) - внешний облик приложения
После рассмотрим файл MainActivity.xml
, для этого
нам нужно найти каталог res>layout>. Откроем
MainActivity.xml
для создания облика первой - главной
страницы и перетягивая спанели Palette необходимые нам типы
объектов.
Советую вам размещать объекты под ConstraintLayout, так объекты можно будет привязывать узелками к родительскому ConstraintLayout, который по умолчанию занимает всю пространство, а после привязки узелков, мы можем размещать объекты на нужном нам вертикальном и горизонтальном выравнивании.
Кстати, вместо px, тут используется другая величина - dp, позволяющая на разных экранах видеть одинаковый и желаемый результат.
Кстати, также, советую названия Текст полей переназначать в String значения, чтобы в дальнейшем было проще делать перевод интерфейса - подобный функционал уже встроен в Android Studio. Для этого нажимаем на объект, далее в меню Свойств объекта находим поле text и нажимаем на маленькую плашку-кнопку справа от текста. В открывшимся окне, нажимаем на плюсик слева сверху и создаем название String-переменной и ее значение по умолчанию:
Создание String-переменнойДля перевода интерфейса, необходимо сохранить изменения и над нашим конструктором Layout нажать на кнопку Default (en-us) и выбрать Edit Translations, далее найти слева сверху значок глобуса и нажать на него для добавления нового языка:
Переводы для интерфейсовТаким образом создадим дополнительные макеты (layouts) для оставшихся двух окон:
Скриншоты: еще два макетаПрограммируем на Java под Android
Еще раз повторюсь, что это Tutorial больше для новичков; дальше я буду комментировать практически каждую строчку. Ссылка на проект на Github будет в конце данной статьи.
Открываем файл Main_Activity.java, который будет отвечать за логику наших переключателей и главного экрана в целом, а она такова:
-
В самом верху должен отображаться пользовательский заголовок, если он настроен.
-
На переключателях должен отображаться тот текст, который пользователь настраивает из окна с макетом Activity_Settings.xml
-
Количество переключателей должно соответствовать заданному числу из окна макета Activity_Advanced.xml
-
После выхода из приложения и повторного запуска все переключатели должны оставаться в том же положении, в котором пользователь их оставил
-
Сброс переключателей возможен только, если переключатель Уверен/-а? включен.
-
А также, должны работать оставшиеся кнопки меню.
Пишем следующее:
package com.bb.myapplication;import androidx.appcompat.app.AppCompatActivity;import androidx.appcompat.widget.SwitchCompat;import android.content.Intent;import android.content.SharedPreferences;import android.graphics.Color;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.TextView;public class MainActivity extends AppCompatActivity { //Создаем 9 переключателей с помощью массива SwitchCompat[] switcharray = new SwitchCompat[9]; Boolean Reset; //Булев для выключения переключателя Button NextButton; public int[] list_of_switches = { R.id.switch_compat1, R.id.switch_compat2, R.id.switch_compat3, R.id.switch_compat4, R.id.switch_compat5, R.id.switch_compat6, R.id.switch_compat7, R.id.switch_compat8, R.id.switch_compat10, //переключатель "Вы уверены?" //8 }; //Нажатие кнопки Сброс public void ResetButtonClick (View view) throws IllegalAccessException { Reset =false; if (switcharray[8].isChecked()) { //Если переключатель "Вы уверены?" нажат, то разрешаем переключить в false остальные переключатели SharedPreferences.Editor editor = getSharedPreferences("save" ,MODE_PRIVATE).edit(); //Сохраняем в Intent значения всех переключателей в False for (int k=0; k<10; k++) { editor.putBoolean("value"+k, false); } editor.apply(); //Устанавливаем все переключатели в значение False for (int i=0;i<9;i++){ switcharray[i].setChecked(false); } //Reset background color of checked SwitchCompats for (int i = 0; i < 9; i++) { findViewById(list_of_switches[i]).setBackgroundColor(Color.TRANSPARENT); } } } //Создание формы / открытие приложения @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //Назначаем полям значения по умолчанию и сохраняем их в Intent String[] tsfield = new String[8]; SharedPreferences prefs = getSharedPreferences("MY_DATA", MODE_PRIVATE); tsfield[0] = prefs.getString("KEY_F0", "Выключил газ"); tsfield[1] = prefs.getString("KEY_F1", "Выключил воду"); tsfield[2] = prefs.getString("KEY_F2", "Покормил кошек"); tsfield[3] = prefs.getString("KEY_F3", "Закрыл окна"); tsfield[4] = prefs.getString("KEY_F4", "Выключил Интернет"); tsfield[5] = prefs.getString("KEY_F5", "Закрыл дверь"); tsfield[6] = prefs.getString("KEY_F6", "Выключил везде свет"); tsfield[7] = prefs.getString("KEY_F7", "Вынес мусор"); //Получаем настройки текста заголовка String hellotext = prefs.getString("hellotitletext", ""); switcharray[6] = findViewById(list_of_switches[6]); switcharray[7] = findViewById(list_of_switches[7]); //Получаем настройки количества полей String sixfields = prefs.getString("sixfields", "true"); String sevenfields = prefs.getString("sevenfields", "false"); String eightfields = prefs.getString("eightfields", "false"); if (sixfields.equals("true")){ switcharray[6].setVisibility(View.GONE); switcharray[7].setVisibility(View.GONE); } else if (sevenfields.equals("true")) { switcharray[6].setVisibility(View.VISIBLE); switcharray[7].setVisibility(View.GONE); } else if (eightfields.equals("true")) { switcharray[6].setVisibility(View.VISIBLE); switcharray[7].setVisibility(View.VISIBLE); } //Создаем массив из TextView TextView[] textarr = new TextView[8]; //Каждому переключателю назначаем текст из итерации поля tsfield for (int i=0; i<8;i++){ textarr[i] = (TextView) findViewById(list_of_switches[i]); textarr[i].setText(tsfield[i]); } //Назначаем текст заголовка TextView textView5 = (TextView) findViewById(R.id.textView5); textView5.setText(hellotext); //Отображать заголовок, если соотв. поле заполнено if(!hellotext.matches("")) { textView5.setVisibility(View.VISIBLE); } //Создаем связь каждого элемента переключателя по id из XML с соответствующей переменной типа SwitchCompat for (int i=0;i<9;i++) { switcharray[i] = findViewById(list_of_switches[i]); } //Создаем связь кнопки по id bt_next из xml переменной NextButton NextButton = findViewById(R.id.bt_next); //Используем SharedPreferences = "save" SharedPreferences sharedPreferences = getSharedPreferences("save" , MODE_PRIVATE); //При первом запуске - все переключатели в False for (int k=0; k<9; k++) { switcharray[k].setChecked(sharedPreferences.getBoolean("value"+k, false)); } //При переключении переключателей сохраняем данные, а также, проверяем их при повторном запуске for (int k=0; k<9; k++) { final int finalK = k; switcharray[k].setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (switcharray[finalK].isChecked()) { //когда переключатель включен Reset = true; // switcharray[finalK].setBackgroundColor(Color.parseColor("#c8a2c6")); SharedPreferences.Editor editor = getSharedPreferences("save" , MODE_PRIVATE).edit(); editor.putBoolean("value" + finalK, true); editor.apply(); switcharray[finalK].setChecked(true); } else { //когда переключатель выключен SharedPreferences.Editor editor = getSharedPreferences("save" , MODE_PRIVATE).edit(); editor.putBoolean("value" + finalK, false); Reset = false; switcharray[finalK].setBackgroundColor(Color.TRANSPARENT); editor.apply(); switcharray[finalK].setChecked(false); } } }); } //Кнопка открытия страницы настроек NextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { //Go to next activity Intent intent2 = new Intent(MainActivity.this, Activity_settings.class); startActivity(intent2); } }); }}
Следующим этапом будет написание кода для корректной работы макета Activity_Settings.XML, а логика его такова:
-
Введенные пользователь записи сохраняются даже после перезапуска приложения
-
Количество полей соответствуют числу, заданному в настройках из макета Activity_Advanced.xml
-
А также, должны работать оставшиеся кнопки меню.
package com.bb.myapplication;import androidx.appcompat.app.AppCompatActivity;import android.content.Intent;import android.content.SharedPreferences;import android.net.Uri;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.TextView;public class Activity_settings extends AppCompatActivity { //Initialize Variable Button btBack; Button fcSubmit; Button btAdvanced; //Ассоциируем поля ввода с переменными с помощью массива EditText[] InputFields = new EditText[8]; //Назначаем полям значения по умолчанию и сохраняем их в Intent String[] tsfield = new String[8]; //Создаем массив элементов из XML по id public int[] list_of_fields = { R.id.inputField0, R.id.inputField1, R.id.inputField2, R.id.inputField3, R.id.inputField4, R.id.inputField5, R.id.inputField6, R.id.inputField7, }; private SharedPreferences prefs; //Создание формы / открытие приложения @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_settings); //Назначаем полям значения по умолчанию и сохраняем их в Intent prefs = getSharedPreferences("MY_DATA", MODE_PRIVATE); //Получаем данные по количеству используемых полей String sixfields = prefs.getString("sixfields", "true"); String sevenfields = prefs.getString("sevenfields", "false"); String eightfields = prefs.getString("eightfields", "false"); EditText inputField71var = (EditText) findViewById(list_of_fields[6]); EditText inputField81var = (EditText) findViewById(list_of_fields[7]); if (sixfields.equals("true")){ inputField71var.setVisibility(View.INVISIBLE); inputField81var.setVisibility(View.INVISIBLE); } else if (sevenfields.equals("true")) { inputField71var.setVisibility(View.VISIBLE); inputField81var.setVisibility(View.INVISIBLE); } else if (eightfields.equals("true")) { inputField71var.setVisibility(View.VISIBLE); inputField81var.setVisibility(View.VISIBLE); } tsfield[0] = prefs.getString("KEY_F0", "Выключил газ"); tsfield[1] = prefs.getString("KEY_F1", "Выключил воду"); tsfield[2] = prefs.getString("KEY_F2", "Покормил кошек"); tsfield[3] = prefs.getString("KEY_F3", "Закрыл окна"); tsfield[4] = prefs.getString("KEY_F4", "Выключил Интернет"); tsfield[5] = prefs.getString("KEY_F5", "Закрыл дверь"); tsfield[6] = prefs.getString("KEY_F6", "Выключил везде свет"); tsfield[7] = prefs.getString("KEY_F7", "Вынес мусор"); //Назначаем полям ввода текст из SharedPreferences for (int i=0; i<8; i++) { InputFields[i] = (EditText) findViewById(list_of_fields[i]); InputFields[i].setText(tsfield[i]); } //Создаем переменные для кнопок btBack = findViewById(R.id.bt_back); fcSubmit = findViewById(R.id.submit_fc); btAdvanced = findViewById(R.id.btAdvanced); //Кнопка Назад btBack.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { //Go back Intent intent = new Intent ( Activity_settings.this,MainActivity.class ); startActivity(intent); } }); //Кнопка Расширенные настройки/Дополнительно btAdvanced.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { //Open Advanced Settings Intent intent = new Intent ( Activity_settings.this,Activity_advanced.class ); startActivity(intent); } }); } //Ссылка-значок на внешний ресурс - ссылка на мой телеграм public void tglink(View view){ Intent myWebLink = new Intent(android.content.Intent.ACTION_VIEW); myWebLink.setData(Uri.parse("https://t.me/EndlessNights")); startActivity(myWebLink); } //Кнопка Сохранить данные public void SaveData(View view) { for (int i=0; i<8;i++) { tsfield[i] = InputFields[i].getText().toString(); SharedPreferences.Editor editor = prefs.edit(); editor.putString("KEY_F"+i, tsfield[i]); editor.apply(); } // Открываем главную страницу startActivity(new Intent(getApplicationContext(), MainActivity.class)); }}
И наконец опишем логику работы последнего окна в приложении - с Дополнительными настройками:
-
Количество полей для отображения - в данном случае выбор с помощью радиокнопок - 6, 7 или 8 полей.
-
Текстовый заголовок, который пользователь может ввести и который будет отображаться на главной странице/активности.
-
Также, текстовый заголовок и выбранное количество полей с помощью радиокнопок должны сохранять свое состояние даже после перезапуска приложения.
-
И наконец должны работать оставшиеся кнопки меню.
package com.bb.myapplication;import androidx.appcompat.app.AppCompatActivity;import android.content.Intent;import android.content.SharedPreferences;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.CompoundButton;import android.widget.EditText;import android.widget.RadioButton;import android.widget.RadioGroup;import android.widget.Switch;import android.widget.TextView;public class Activity_advanced extends AppCompatActivity { Button btBack; //Назначаем радиокнопкам значения по умолчанию Boolean sixbool = true; Boolean sevenbool = false; Boolean eightbool = false; private SharedPreferences prefsadv; //Поле ввода текста для заголовка private EditText hellotitletext; RadioGroup rdGroup; //Переменные для радиокнопок public RadioButton r1, r2, r3; //Переменные для передачи состояния из boolean в sharedPrefs String sixdata; String sevendata; String eightdata; Switch bgswitchvar; private SharedPreferences prefs; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_advanced); bgswitchvar = findViewById(R.id.bgswitch); prefsadv = getSharedPreferences("MY_DATA", MODE_PRIVATE); rdGroup = (RadioGroup)findViewById(R.id.radioGroup); //Поле заголовка String hellotitletext1 = prefsadv.getString("hellotitletext",""); hellotitletext = (EditText) findViewById(R.id.hellotitletext); hellotitletext.setText(hellotitletext1); //Ассоциируем переменные с полями по id из xml r1 = findViewById(R.id.sixfields); r2 = findViewById(R.id.sevenfields); r3 = findViewById(R.id.eightfields); //При нажатии на радиокнопку, вызываем функцию Update с заданным ключом r1.setChecked(Update("rbsix")); r2.setChecked(Update("rbseven")); r3.setChecked(Update("rbeight")); //При нажатии первой кнопки добавляем True с ключом rbsix в RBDATA r1.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean r1_isChecked) { SaveIntoSharedPrefs("rbsix", r1_isChecked); } }); //При нажатии второй кнопки добавляем True с ключом rbsix в RBDATA r2.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean r2_isChecked) { SaveIntoSharedPrefs("rbseven", r2_isChecked); } }); //При нажатии третьей кнопки добавляем True с ключом rbsix в RBDATA r3.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean r3_isChecked) { SaveIntoSharedPrefs("rbeight", r3_isChecked); } }); //Back button btBack = findViewById(R.id.btBackadvanced); btBack.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { //Go back Intent intent = new Intent ( Activity_advanced.this,Activity_settings.class ); startActivity(intent); } }); } //Сохранение данных в SharedPreferences - ожидая ключ и значение булева типа private void SaveIntoSharedPrefs(String key, boolean value){ SharedPreferences sp = getSharedPreferences("RBDATA",MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putBoolean(key,value); editor.apply(); } //Функция обновления значения в SharedPreferences private boolean Update(String key){ SharedPreferences sp = getSharedPreferences("RBDATA",MODE_PRIVATE); return sp.getBoolean(key, false); } //Сохраняем данные по количеству полей public void SaveDataAdvanced(View view) { int checkedId = rdGroup.getCheckedRadioButtonId(); if(checkedId == R.id.sixfields) { sixbool = true; sevenbool = Boolean.FALSE; eightbool = Boolean.FALSE; } else if (checkedId == R.id.sevenfields){ sevenbool = true; sixbool = Boolean.FALSE; eightbool = Boolean.FALSE; } else if (checkedId == R.id.eightfields){ eightbool = true; sevenbool = Boolean.FALSE; sixbool = Boolean.FALSE; } sixdata = String.valueOf(sixbool); sevendata = String.valueOf(sevenbool); eightdata = String.valueOf(eightbool); String hellofield = hellotitletext.getText().toString(); SharedPreferences.Editor editor = prefsadv.edit(); editor.putString("sixfields", sixdata); editor.putString("sevenfields", sevendata); editor.putString("eightfields", eightdata); editor.putString("hellotitletext", hellofield); editor.apply(); startActivity(new Intent(getApplicationContext(), MainActivity.class)); }}
Подготовка приложения к публикации
Для отладки и проверки работоспособности приложения советую вам использовать настоящее устройство на Android, так вы сразу сможете отследить наличие, как минимум проблем с оформлением.
Здесь я приложил видео-инструкцию, как подключить свой смартфон к Android studio для отладки вашего приложения. На видео вы можете заметить первую версию данного приложения с очень плохим кодом:
Регистрация в Google Play
Для публикации приложения нам следует создать специальный аккаунт разработчика, вот прямая ссылка.
Далее вам предстоит оплатить пошлину в $35 за возможность публиковать приложения, это почти в 3 раза дешевле, чем в Steam, при том, что Steam просит $100 за каждое публикуемое приложение/игру, даже бесплатное, а с аккаунтом разработка, в Google Play вы можете публиковать несчётное множество приложений.
После оплаты и успешной авторизации в консоли разработчика, необходимо нажать на синюю кнопку "Создать приложение", далее заполнить все необходимые поля:
После создания приложения в консоли разработчика Google Play, необходимо перейти в раздел Рабочая версия и нажать на кнопку Создать новый выпуск. Вам предложат получить электронную подпись для вашего приложения с расширением *.jks, с помощью которой вам предстоит подписать свое первое приложение, а также, все дальнейшие выпуски с обновлениями.
Возвращаемся в Android Studio и необходимо заполнить немного
информации о нашем приложении, для этого нажимаем File>Project
Structure и заполняем поля Version Code и Version Name - без них
Google Play Google Play не допустит ваше приложение до
публикации:
Наконец, переходим в следующий раздел: пункт меню Build>Generate Signed Bundle / APK
В открывшимся окне выбираем APK. В подразделе Key Store Path выбираем Create new, далее заполняем все поля (прямая ссылка на официальную инструкцию), далее данный ключ потребуется загрузить в консоль Google Play. Затем вернемся в Android Studio и после ввода всех необходимых данных, нажимаем Next
В следующем окне отмечаем все чекбоксы, выбираем release и нажимаем Finish - Android Studio скомпилирует подписанное приложение, которое можно опубликовать в Google Play.
Итог
После загрузки файла приложения APK потребуется заполнить множество форм и подготовить множество материалов: описание на разных языках (если необходимо), изображения на разных языках (надписи на изображениях я имею в виду), логотипы, иконки разных размеров, скриншоты со смартфона и планшета.
Наконец отправляем приложение в публикацию. Сотрудники Google Play будут проверять ваше приложение в течении 2 недель, судя по официальным данным. Данное приложение рассматривали в течении 5 суток. Также, стоит учесть, что каждое обновление, также, будут проверять, но на обновления уходит не более 2-3 суток.
Ссылка на GitHub, как обещано. Ссылка на приложение в Google Play.