Обзор приложения «Вопрос-ответ»

Мобильное приложение для Андроид устройств «Вопрос-ответ». Его я разработал в результате прохождения бесплатного мини-курса на образовательной платформе Нетология. Курс «Разработка мобильных приложений с нуля» не даёт обширных знаний, но позволяет создать полноценное мобильное приложение. Разработка идёт в Андроид студии на языке Котлин.

рис. 1 «Вопрос-ответ» в работе.

Идея приложения довольна тривиальна. Пользователь задает свой вопрос по-английски. Вопрос можно написать в поле ввода или задать голосом, нажав на зеленоватую кнопку с микрофоном. Приложение переадресует вопрос к системе WolframAlpha по API. Полученные ответы будут отображены списком, который можно прочитать пролистав вниз или можно прослушать выбранный касанием ответ. Пользователь будет проинформирован, если ответ не удалось получить из-за отсутствия подключения к системе или некорректного вопроса. Прослушивание длинного ответа можно прервать по кнопке «Стоп» в меню. Для тех, кто английским языком не владеет, пользы от применения приложения будет мало. Вопросы перевода с языка на язык тоже можно решить, но это уже выходит за рамки данного приложения.

Технологии

В процессе разработки приложения удалось ознакомиться и применить следующие технологии :

  • Использование элементов управления библиотеки «Material Design»
  • Использование сервиса WolframAlpha по API
  • Создание пользовательского меню
  • Использование плавающей кнопки FloatingActionButton
  • Использование элементов ввода и отображения списка
  • Применение Coroutine для длительных операций сетевого обмена
  • Отображение кругового ProgressBar во время длительной операции сетевого обмена
  • Задействование встроенного голосового интерфейса Андроид для получения вопросов и озвучивания ответов — служба распознавания речи (класс SpeechRecognizer) и синтезатор речи (класс TextToSpeech)
  • Создание и применение пользовательской иконки приложения

Код

Приведу код единственного файла на языке Котлин, в котором и реализовано всё вышеперечисленное (ну кроме макета, разумеется, хотя и там всё просто) :

package com.bignerdranch.android.netologingrid

import android.content.Intent
import android.content.SharedPreferences
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.speech.RecognizerIntent
import android.speech.tts.TextToSpeech
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.ListView
import android.widget.ProgressBar
import android.widget.SimpleAdapter
import android.widget.TextView
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import com.wolfram.alpha.WAEngine
import com.wolfram.alpha.WAPlainText
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
import kotlin.collections.HashMap

private const val TAG: String = "MainActivity"

private const val wf_key = "*****-**********" 
// укажите свой, выданный для приложения сервисом 

class MainActivity : AppCompatActivity() {

    lateinit var requestInput: TextInputEditText

    lateinit var podsAdapter: SimpleAdapter

    lateinit var progressBar: ProgressBar

    lateinit var waEngine: WAEngine

    lateinit var textToSpeech: TextToSpeech

    var isTtsReady = false

    val VOICE_RECOGNATION_REQUEST_CODE: Int = 777

    val pods = mutableListOf<HashMap<String, String>>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Log.d(TAG, "start of onCreate function")

        initViews()
        initWolframEngine()
        initTts()

        Log.d(TAG, "end of onCreate function")
    }

    fun initViews() {
        val toolbar: MaterialToolbar = findViewById(R.id.toolbar)
        setSupportActionBar(toolbar)

        requestInput = findViewById(R.id.text_input_edit)
        requestInput.setOnEditorActionListener { v, actionID, event ->
            if (actionID == EditorInfo.IME_ACTION_DONE) {
                pods.clear()
                podsAdapter.notifyDataSetChanged()

                val question = requestInput.text.toString()
                askWolfram(question)
            }

            return@setOnEditorActionListener false
        }

        val podsList: ListView = findViewById(R.id.pods_list)
        podsAdapter = SimpleAdapter(
            applicationContext,
            pods,
            R.layout.item_pod,
            arrayOf("Title", "Content"),
            intArrayOf(R.id.title, R.id.content)
        )
        podsList.adapter = podsAdapter
        podsList.setOnItemClickListener { parent, view, position, id ->
            if (isTtsReady) {
                val title = pods[position]["Title"]
                val content = pods[position]["Content"]
                textToSpeech.speak(content, TextToSpeech.QUEUE_FLUSH, null, title)
            }
        }

        val voiceInputButton: FloatingActionButton = findViewById(R.id.voice_input_button)
        voiceInputButton.setOnClickListener {
            Log.d(TAG, "FAB")
            pods.clear()
            podsAdapter.notifyDataSetChanged()

            if (isTtsReady) {
                textToSpeech.stop()
            }

            showVoiceInputDialog()
        }

        progressBar = findViewById(R.id.progress_bar)
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.toolbar_menu, menu)
        return super.onCreateOptionsMenu(menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId) {
            R.id.action_stop -> {
                Log.d(TAG, "action_stop")
                if (isTtsReady) {
                    textToSpeech.stop()
                }
                return true
            }
            R.id.action_clear -> {
                requestInput.text?.clear()
                pods.clear()
                podsAdapter.notifyDataSetChanged()
                return true
            }
        }
        return super.onOptionsItemSelected(item)
    }

    fun initWolframEngine() {
        waEngine = WAEngine().apply {
            appID = wf_key
            addFormat("plaintext")
        }
    }

    fun showSnackbar(message: String) {
        Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_INDEFINITE).apply {
            setAction(android.R.string.ok) {
                dismiss()
            }
            show()
        }
    }

    fun askWolfram(request: String) {
        progressBar.visibility = View.VISIBLE
        CoroutineScope(Dispatchers.IO).launch {
            val query = waEngine.createQuery().apply { input = request }
            kotlin.runCatching {
                waEngine.performQuery(query)
            }.onSuccess { result ->
                withContext(Dispatchers.Main) {
                    progressBar.visibility = View.GONE
                    if (result.isError) {
                        showSnackbar(result.errorMessage)
                        return@withContext
                    }

                    if (!result.isSuccess) {
                        requestInput.error = getString(R.string.error_do_not_understand)
                        return@withContext
                    }

                    for(pod in result.pods) {
                        if (pod.isError) continue
                        val content = StringBuffer()
                        for(subpod in pod.subpods) {
                            for(element in subpod.contents) {
                                if (element is WAPlainText) {
                                    content.append(element.text)
                                }
                            }
                        }
                        pods.add(0, HashMap<String, String>().apply {
                            put("Title", pod.title)
                            put("Content", content.toString())
                        })
                    }

                    podsAdapter.notifyDataSetChanged()
                }
            }.onFailure { t ->
                withContext(Dispatchers.Main) {
                    progressBar.visibility = View.GONE
                    showSnackbar(t.message ?: getString(R.string.error_something_went_wrong))
                }
            }
        }
    }

    fun initTts() {
        textToSpeech = TextToSpeech(this) { code ->
            if (code != TextToSpeech.SUCCESS) {
                Log.e(TAG, "TTS error code: $code")
                showSnackbar(getString(R.string.error_tts_is_not_ready))
            } else {
                isTtsReady = true
            }
        }
        textToSpeech.language = Locale.US
    }

    fun showVoiceInputDialog() {
        val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
            putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
            putExtra(RecognizerIntent.EXTRA_PROMPT, getString(R.string.request_hint))
            putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.US)
        }

        kotlin.runCatching {
            startActivityForResult(intent, VOICE_RECOGNATION_REQUEST_CODE)
        }.onFailure { t ->
            showSnackbar(t.message ?: getString(R.string.error_voice_recognition_unavailable))
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == VOICE_RECOGNATION_REQUEST_CODE && resultCode == RESULT_OK) {
            data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.get(0)?.let { question ->
                requestInput.setText(question)
                askWolfram(question)
            }
        }
    }
}

Конечно, приложению «Вопрос-ответ» до Алисы или Маруси ещё очень далеко. Но построив такое приложение, разработчик уже будет понимать технические принципы работы подобных приложений. Вряд ли наше приложение при на наличии более тяжеловесных коллег «по цеху» будет применимо на практике. Но в учебных целях его разработка очень полезна.

Успехов Вам на просторах Котлин Андроид разработки !

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *