Сегодня статья о гибридных Android-приложениях для малышей. Во всех смыслах этих слов. Мы поговорим о написании простейшего гибридного (Java+HTML+Javascript) Android приложения для опросов учеников начальных классов об их рюкзаках. Предполагается минимальное знание основ Java, HTML и JavaScript. Если Вы Android-разработчик, хоть с минимальным опытом – Вам эта статья вряд ли будет интересна, можно не открывать. Всех остальных, кто еще только начинает или думает начать разработку под Android, и кому интересны основы разработки под Android, прошу под кат.

Вводная. Дочке (2 класс) было поручено сделать исследовательскую работу на тему «Влияние веса рюкзака на здоровье ребенка». Естественно, в силу возраста, основная работа пришлась на родителей. Решили провести опрос в классе на предмет того, у кого сколько весит рюкзак, кто сам сколько весит (для вычисления нормы веса рюкзака, который не должен превышать 10% от массы ребенка), кто носит рюкзак в школу и так далее. Для того, чтобы разнообразить школьные будни, решил сделать приложение под телефон на Android, который есть у дочки, приложение для опроса. Изначально планировалось включить в опросник вес рюкзака и детеныша, но не успел, и по итогу эти параметры записали на листик, по старинке. Остались только те вопросы, на которые детеныши могли ответить самостоятельно.

Суть задачи: разработать приложение для опроса младшеклассников для создания презентации дочке о том, насколько вредно носить тяжелые рюкзаки. На картинке выше можно увидеть то, что у нас по итогу получится.
Сразу оговорюсь, обычно я разрабатываю нативные приложения для Android, HTML чисто для Web-приложений, но в этот раз было решено разработать гибридное приложение так как во-первых, быстрее для данной задачи, а сроки были предельно сжатые, во-вторых, это было удобней с точки зрения функционала приложения, в третьих, это был первый проект разрабатываемый в Android Studio, хотелось минимизировать возможные проблемы при использовании нового инструмента, чтобы закончить вовремя.

Итак, приступим. Для начала, разумеется, Java-код (пояснения в комментариях), естественно не забываем добавить WebView в нашу Activity, присваиваем ему id webView:



package com.probosoft.survey;

import android.os.Build;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.Window;
import android.webkit.WebSettings;
import android.webkit.WebView;

public class MainActivity extends AppCompatActivity {

    // Перегружаем метод onCreate, в нем создаем необходимый нам WebView и устанавливаем ему возможность масштабирования
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main); // Показываем нашему приложению, кто тут главный
        WebView wv = (WebView) findViewById(R.id.webView); // Создаем WebView – мини-браузер в приложении
        WebSettings settings = wv.getSettings(); // Получаем класс настроек нашего мини-браузера
        settings.setDisplayZoomControls(true); // Разрешаем показать настройки масштабирования для мини-браузера
        wv.loadUrl("file://android_asset/html/index.html"); // Загружаем страницу нашего приложения в мини-браузер
    }
}

Помещаем тестовый index.html в папку assets/html. Пробуем запустить. Ничего не получается. Выясняем важный момент, что при обращении к внутренним ресурсам слешей после протокола должно быть не два, а три. Меняем:

        wv.loadUrl("file://android_asset/html/index.html");

на:

        wv.loadUrl("file:///android_asset/html/index.html");

Ура! Все загрузилось. Начинаем писать HTML и JS код.

<!DOCTYPE html>
<html>
  <head>
    <title>Survey</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- Bootstrap -->
    <link href="vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen">
    <link href="vendor/bootstrap/css/bootstrap-theme.css" rel="stylesheet" media="screen">
    <link href="vendor/jquery/jquery-ui.min.css" rel="stylesheet" media="screen">
    <link href="vendor/jquery/jquery-ui.theme.css" rel="stylesheet" media="screen">
    <link href="css/main.css" rel="stylesheet" media="screen">
    <style>
       #menu {
          width: 100%;
       }
    </style>
  </head>
  <body>
<!—Тут подключаем jQuery и Bootstrap -->
    <script src="vendor/jquery/external/jquery/jquery.js"></script>
    <script src="vendor/jquery/jquery-ui.min.js"></script>
    <script src="vendor/bootstrap/js/bootstrap.min.js"></script>

<!—Здесь подключаем классы приложения -->
    <script src="js/consts.js"></script>
    <script src="js/respondents.js"></script>
    <script src="js/survey.js"></script>
    <script src="js/questions.js"></script>
    <script src="js/admin.js"></script>

    <script src="data/respondents.js"></script>
    <script src="data/questions.js"></script>

<div id="menuDiv">
    <div class="page-header" onclick="javascript: showResults ();">
        <center><h3 id="title"></h3></center>
    </div>
    <div style="display: none;" id="clearButton">
        <input type="button" value="Clear all" onclick="javascript: clearAll ();"/><br/><br/>
    </div>
<!—- Дабы не смущать школьников и преподователя, кнопка для демонстрации результатов по умолчанию скрыта -->
    <div style="display: none;" id="showResults">
        <input type="button" value="Show results" onclick="javascript: showResults (true);"/><br/><br/>
    </div>
<!—- Область для отображения списка учеников http://www.w3schools.com/bootstrap/bootstrap_list_groups.asp -->
    <div id="mainPane">
        <ul class="list-group" id="menu">
        </ul>
    </div>
<!—Область для отображения результатов -->
    <div id="resultsPane" style="display: none;">
        <form method="post" action="http://serj.by/survey/api/storeSurveyData.php" id="storeForm">
            <textarea name="surveyData" id="surveyData" style="width: 100%; height: 100%;" rows=25>
            </textarea>
            <input type="submit" value="Store on server"/>
            <input type="hidden" name="redirectURL" value="."/>
        </form>
    </div>
</div>
<div id="thanks">Спасибо за ответы!<br/><br/><input type="button" title="Ok" value="Пожалуйста!" id="ok"/></div>
<script>

       var respondents;

var adminMode = true; // 

function clearAll () {
   try {
       if(typeof(Storage) !== "undefined") {
          this.storage = localStorage;
       }
   } catch (e) {
      alert ("Local storage error: "+e);
   }
   this.storage.clear ();
}

function init () {
    $("#mainPane").show ();
    $("#resultsPane").hide ();
    if (adminMode) $("#showResults").show ();

   $("#title").html ("Выберите ученика");
   var res = dataRespondents;

    res.forEach (function (element, i, arr) {
      element.id = i+1;
    });

   respondents = new Respondents (res);
   respondents.renderRespondents ($("#menu"));

   $("#storeForm redirectURL").val (document.location.href);
}

init ();
</script>
  </body>
</html>

Сначала я пробовал использовать AJAX для загрузки данных, но довольно быстро убедился, что внутри WebView, и на локальных ресурсах он попросту не работает. Поэтому, для загрузки контента пришлось использовать довольно противоречивый метод – сохранить все данные о респондентах в глобальный массив.
Пробуем запускать. Опять не работает. В чем же дело? Для нашего WebView мы не разрешили выполнение JavaScript. Исправим. Добавим к Java коду:
	settings.setJavaScriptEnabled(true);

Теперь работает. Ура! Нам нужно было разрешить исполнение JavaScript в нашем WebView. Пишем классы функционала. Код приводить не буду. С ним можно познакомиться на GitHub проекта (в конце статьи). Здесь же описываю основные проблемы, с которыми может столкнуться начинающий разработчик гибридных приложений.

Далее мы подключаем LocalStorage для сохранения данных наших анкет. Для этого мы используем «класс» Survey в survey.js. Традиционно, комментарии к коду в нем самом.

/**
 * Represents survey for particular respondent
 * @param integer id Id of respondent
 */
var Survey = function (in_respondentId, in_respondent)
{
   var respondentId; // Id опрашиваемого ученика
   var respondent = null; // Объект, представляющий опрашиваемого ученика
   var questions = null; // Массив вопросов
   
   var parent = this; // Грязный хак, позволяющий получить контекст объекта внутри вложенных контекстов
   
   var storage = null; // Переменная содержжащая LocalStorage на случай если его придется повторно использовать
   
   this.answers = []; // Массив ответов на вопросы опросника
   
   /**
    * Begins survey for chosen respondent
    */
   this.start = function ()
   {     
         var res = dataQuestions; // Инициализируем  переменную с вопросами
         
         parent.questions = new Questions (res, parent.respondent); // Создаем первый вопрос
         parent.questions.start (); // Приступаем к опросу
   }
   
/**
* Stores all answers in storage
*/
   this.collectAnswersAndStore = function ()
   {
      this.storage.setItem (window.UNIQUE_STORAGE_ID+this.respondentId, JSON.stringify (this.answers)); // Сохраняем результат опроса
      window.init ();  // Возвращаемся на главный экран
   }
   
   this.surveyOption = function (val)
   {
      this.answers.push (val); // Запоминаем ответ ученика
      //alert (this.answers);
      if (!this.questions.advanceQuestion ()) // Проверяем, есть ли еще вопросы
      {
         this.collectAnswersAndStore (); // Если нет больше вопросов, сохраняем ответы
      }
   }

  // Инициализация переменных
   this.respondentId = in_respondentId;
   this.respondent = in_respondent;

  // Пытаемся получить доступ к хранилищу
   try {
       if(typeof(Storage) !== "undefined") {
          this.storage = localStorage; 
       }
   } catch (e) {
      alert ("Local storage error: "+e);
   }
}

Все вроде как и работает, но ничего не сохраняется. Попутно узнаем забавную подробность – в последних версиях Android alert в WebView делает… ничего. Совсем ничего. Ни ошибки, ни какого-то сообщения в консоли. Просто как-будто его и нет. Выясняем, что для использования LocalStorage в WebView нам нужна установка дополнительных флагов для WebView. Сделаем это:


     settings.setDomStorageEnabled(true);
     settings.setDatabaseEnabled(true);

Ура! LocalStorage заработал. Долго ли коротко ли, за выходные что-то пригодное для использования было написано. Ребенок был отправлен в гимназию с телефоном, на котором было установлено данное поделие. Ребята (уж с помощью учительницы, или самостоятельно – это осталось за кадром) добросовестно прошли анкетирование, и не было данных только по четырем ученикам, которые по тем или иным причинам отсутствовали на занятиях.

Теперь передо мной встала проблема: нужно как-то извлечь данные (да-да, об этом нужно было думать изначально, но не забываем, что приложение разрабатывалось в жутком цейтноте, и предполагалось, что эта задача не из сложных и не из неотложных и ее вполне можно решить потом).

Основная проблема оказалась в том, что у дочки довольно старый и слабый телефон (выбирался с учетом фактора «чтобы не жалко было чуть что»). Пробовал вытащить данные через Bluetooth, отправкой AJAX-запроса на сервер, создавалась форма для отправки и т.д. – без вариантов. Текст в DIV’е не выбирается, по итогу DIV был переделан в TextArea (что можно наблюдать в финальном коде на GitHub). Оттуда удалось выделить и скопировать текст с результатами опроса и переслать его на мой E-mail. По итогу это оказался единственный рабочий вариант.

Пишем скрипт, чтобы данные оказались в Excel таблице. На данном этапе выявилась еще одна проблема изначальной архитектуры – те ученики, которые не проходили анкетирование помечались простой строкой «Анкета не заполнена». Естественно, regexp’ы, которые были рассчитаны на нормальные данные на этих строках «спотыкались». Благо таких строк было всего четыре. Вручную они были удалены из итоговых результатов и мы получили вполне адекватную выборку (реальные имена заменены на плейсхолдеры):

Ученик 1, имя: Когда как,Никогда,Мне все нравится в моем рюкзаке 2. Ученик 2, имя: Когда как,Иногда попадаются,Слишком тяжелый 4. Ученик 3, имя: Взрослые,Иногда попадаются,Просто неудобно, но объяснить не могу 5. Ученик 5, имя: Я,Никогда,Мне все нравится в моем рюкзаке 6. Ученик 6, имя: Я,Иногда попадаются,Мне все нравится в моем рюкзаке 7. Ученик 7, имя: Я,Иногда попадаются,Мне все нравится в моем рюкзаке 8. Ученик 8, имя: Я,Никогда,Мне все нравится в моем рюкзаке 9. Ученик 9, имя: Я,Иногда попадаются,Мне все нравится в моем рюкзаке 10. Ученик 10, имя: Я,Иногда попадаются,Мне все нравится в моем рюкзаке 11. Ученик 11, имя: Я,Иногда попадаются,Мне все нравится в моем рюкзаке 12. Ученик 12, имя: Я,Иногда попадаются,Мне все нравится в моем рюкзаке 14. Ученик 14, имя: Когда как,Никогда,Мне все нравится в моем рюкзаке 16. Ученик 16, имя: Я,Всегда что-нибудь есть,Мне все нравится в моем рюкзаке 17. Ученик 17, имя: Я,Иногда попадаются,Мне все нравится в моем рюкзаке 18. Ученик 18, имя: Я,Всегда что-нибудь есть,Мне все нравится в моем рюкзаке 19. Ученик 19, имя: Я,Иногда попадаются,Просто неудобно, но объяснить не могу 21. Ученик 21, имя: Когда как,Всегда что-нибудь есть,Мне все нравится в моем рюкзаке 22. Ученик 22, имя: Я,Никогда,Слишком тяжелый 23. Ученик 23, имя: Я,Иногда попадаются,Мне все нравится в моем рюкзаке 24. Ученик 24, имя: Я,Иногда попадаются,Слишком тяжелый

Можно было, конечно, преобразовать все это на телефоне, но так показалось проще. Это уже легко разбирается простым регулярным выражением. Пишем PHP-скрипт:

<pre>
<?php

function normLastOption ($s)
{
	switch ($s)
	{
		case "Мне":
			return "Мне все нравится в моем рюкзаке";
		case "Слишком":
			return "Слишком тяжелый";
		case "Просто":
			return "Просто неудобно, но объяснить не могу";
	}
}

$results = [];
 $data = "Данные”;
preg_match_all ("/(((\d+)\. (.+), (.+): (.+),(.+),(.+)))+ /U", $data, $results);
print_r ($results);
$csv = "";
foreach ($results [3] as $key => $value)
{
	$csv .= "Ученик ".($key+1).",".$results [6] [$key].",".$results [7] [$key].",".normLastOption($results [8] [$key])."\n";
}
print $csv;
$f = fopen ("survey.csv", "w");
fwrite ($f, $csv);
fclose ($f);
?>

Как видно из кода, последний параметр за счет присутствия пробелов изначально парсился неправильно. Как тут написать правильное регулярное выражение, я не стал заморачиваться опять же в связи с отсутствием времени. Если кто подскажет в комментариях – большое спасибо!

Итак, приложение, не смотря на всю его простоту, полностью выполнило свою функцию — мы собрали данные о том как дети используют свои рюкзаки. Данные были включены в отчет и презентацию и успешно одобрены преподавателем.

Таким образом, мы разработали простое гибридное приложение для Android и даже вытащили из него данные.
Полный код приложения на Github – Лицензия MIT. Если кому нужно такое поделие «на коленке» — используйте на здоровье!
Хотели бы Вы увидеть здесь продолжение цикла статей «Программирование для малышей» (довольно простые программы для детей)?

Проголосовало 23 человека. Воздержалось 8 человек.

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

Поделиться с друзьями
-->

Комментарии (8)


  1. zcasper
    17.10.2016 14:15
    +5

    Всё же лучше для таких штук использовать Cordova/PhoneGap и не лезть в JAVA недры…

    Профитом будет мобильная кроссплатформенность и дополнительно, легко подключаемые плагины для работы с чем угодно будь-то сторонние апи или прослойки над устройствами.


    1. Serj_By
      17.10.2016 14:34

      Это, знаете, все же кому как. Мне, как, по большому счету, нативному разработчику под Android, было проще кинуть в активити WebView, чем тащить за собой такого монстра, как PhoneGap. Тем более, что для такого «серьезного» приложения, этого более чем достаточно. Цель приложения — запустить его один раз и забыть про него. Соответственно, никакой кроссплатформенности тут не нужно. А будет нужно — на iOS можно проделать ровно тоже самое. Как дела обстоят на других мобильных платформах, я не очень в курсе, если честно, но не думаю, что ситуация кардинально отличается. В случае появления необходимости в нативных API, всегда можно воспользоваться интерфейсами.


  1. muxa_ru
    17.10.2016 15:02

    А чем, в данном случае, это «гибридное приложение» лучше простой веб-страницы?

    HTML, JavaScript, php — всё же есть, что бы просто в браузере выполнить.


    1. Serj_By
      17.10.2016 15:07
      +2

      Лишь тем, что оно работает в офлайн-режиме на телефоне, на котором нет интернета, и его удобно запускать из основного меню телефона. А в остальном — да, это лишь HTML-страница с Javascript'ом, ничего более, как, впрочем, и большинство гибридных приложений.


    1. Mihail57
      17.10.2016 15:07

      Думаю, что сыграло роль отсутствие доступа в интернет (или очень медленное соединение), ну и автор пытался опробовать новую совокупность технологий.


      1. Serj_By
        17.10.2016 15:08
        -1

        Абсолютно верно.


  1. bushart
    17.10.2016 21:05
    +1

    По моему в статьей не хватает очень актуальной на сегодняшний день вещи — вывода о том на сколько приложение оказалось полезным?


    1. Serj_By
      18.10.2016 09:15

      Спасибо за замечание! Добавил в конце статьи. В принципе, приложение «одноразовое», о какой-то глобальной полезности речи идти не может. Но свою функцию выполнило на «отлично». Хотя есть в мыслях сделать на его основе «взрослое» приложение, с сервером, созданием своих опросов и так далее. Тогда и можно будет говорить о полезности. Но это, конечно, уже совсем другой уровень. Здесь была цель за пару вечеров сделать что-то, что можно минимально использовать, сплошной хардкод.