Предисловие
Я не являюсь профессиональным разработчиком с огромным стажем в данной области (и это даже не хобби, а лишь нужда в разработке конкретного приложения), потому данная статья, полагаю, будет полезна новичкам, таким же, как и я был в начале разработке своего приложения. Возможно, кто-то найдет что-то полезное из данной статьи, какие-то кусочки окажутся частью ваших будущих разработок.
Я расскажу вам как написать простенькое ToDo-приложение на Android с тремя активностями (рабочими экранами).
Ссылка на проект на Github будет в конце данной статьи.
Установка и первичная настройка
Для разработки приложения я рассмотрю использование бесплатной IDE Intellij от разработчиков 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-переменной и ее значение по умолчанию:
Для перевода интерфейса, необходимо сохранить изменения и над нашим конструктором Layout нажать на кнопку Default (en-us) и выбрать Edit Translations, далее найти слева сверху значок глобуса и нажать на него для добавления нового языка:
Таким образом создадим дополнительные макеты (layouts) для оставшихся двух окон:
Скриншоты: еще два макета
Программируем на Java под Android
Еще раз повторюсь, что это Tutorial больше для новичков; дальше я буду комментировать практически каждую строчку. Ссылка на проект на Github будет в конце данной статьи.
Открываем файл Main_Activity.java, который будет отвечать за логику наших переключателей и главного экрана в целом, а она такова:
В самом верху должен отображаться пользовательский заголовок, если он настроен.
На переключателях должен отображаться тот текст, который пользователь настраивает из окна с макетом Activity_Settings.xml
Количество переключателей должно соответствовать заданному числу из окна макета Activity_Advanced.xml
После выхода из приложения и повторного запуска все переключатели должны оставаться в том же положении, в котором пользователь их оставил
Сброс переключателей возможен только, если переключатель Уверен/-а? включен.
А также, должны работать оставшиеся кнопки меню.
Пишем следующее:
Код под спойлером: 156 строчек
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
А также, должны работать оставшиеся кнопки меню.
Код по спойлером: 124 строчки
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 полей.
Текстовый заголовок, который пользователь может ввести и который будет отображаться на главной странице/активности.
Также, текстовый заголовок и выбранное количество полей с помощью радиокнопок должны сохранять свое состояние даже после перезапуска приложения.
И наконец должны работать оставшиеся кнопки меню.
Код под спойлером: 134 строчки
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.