Сегодня статья о гибридных 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. Если кому нужно такое поделие «на коленке» — используйте на здоровье!
Автор: Serj_By