Предисловие

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

Я расскажу вам как написать простенькое 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-переменной и ее значение по умолчанию:


Создание String-переменной
Создание String-переменной

Для перевода интерфейса, необходимо сохранить изменения и над нашим конструктором Layout нажать на кнопку Default (en-us) и выбрать Edit Translations, далее найти слева сверху значок глобуса и нажать на него для добавления нового языка:


Переводы для интерфейсов
Переводы для интерфейсов

Таким образом создадим дополнительные макеты (layouts) для оставшихся двух окон:

Скриншоты: еще два макета
Макет Activity_Settings.xml
Макет Activity_Settings.xml

Макет Activity_Advanced.xml
Макет Activity_Advanced.xml

Программируем на 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.