Aplikacja moblina – One word weather app

Jak stworzyć aplikację mobilną w Kotlinie?

Pokażę zalety korzystania z Kotlina na androidzie. Wykorzystam praktycznie biblioteki: Anko, GSON, URL(Java). Aplikacja mobilna będzie korzystała z połączenia internetowego. Pobierała dane od użytkownika, parsowała odpowiedź z internetu. Wykorzystywała wielowątkowość i prezentowała dane użytkownikowi. Brzmi groźnie szczególnie dla początkujących. Jednak sposób pokazany w poście to nie jedyna słuszna droga. Warto wiedzieć jak korzystać z tych wszystkich funkcji wprost przez API androida. Programowanie to biznes. Programiści to rzemieślnicy naszych czasów. Więc czasem warto znać te szybkie metody wytwarzania oprogramowania. Mniej linii kodu to też statystycznie mniejsza możliwość błędu.

Do sedna

Tworzymy nowy projekt w Android Studio. Jeśli korzystamy z wersji 3.0 możemy skonwertować kod na Kotlina. Jeśli nie potrzebujemy do tego specjalnej wtyczki. W zakładce Code klikamy Convert Java File to Kotlin File.
Do manifestu dodajemy:

<uses-permission android:name="android.permission.INTERNET" />

Tworzymy prosty layout

Potrzebujemy TextView, EditText i Button. W domyslnie wybranym layoucie czyli ConstraintLayout układamy elementy tak jak nam pasuje np.
Layout
Dorzycam jak wygląda layout w moim projekcie:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/grey"
    tools:context="pl.kotliners.mystartapp.MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginTop="76dp"
        android:padding="10dp"
        android:text=""
        android:textSize="24sp"
        app:layout_constraintBottom_toTopOf="@+id/button"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/etCityName"
        app:layout_constraintVertical_bias="0.312" />

    <EditText
        android:id="@+id/etCityName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginTop="67dp"
        android:ems="10"
        android:hint="Enter city name"
        android:inputType="textPersonName"
        android:text=""
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintHorizontal_bias="0.503"
        app:layout_constraintVertical_bias="0.627"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="144dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:onClick="getWeatherCondition"
        android:text="GET WEATHER"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintVertical_bias="0.322"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp" />

</android.support.constraint.ConstraintLayout>

Mamy layout, zanim zaczniemy kodować całą aplikację musimy zadbać o zależności tzn. powiadomić gradle jakich bibliotek chcemy używać. Czyli w plki build.gradle tam gdzie mamy zależności naszego projektu (powinno się wyświetlać w AS (Project: name_of_project).
Do już zastanych zależności dokładamy:

compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:+"
compile "org.jetbrains.anko:anko:+"
compile 'com.github.salomonbrys.kotson:kotson:+'

Warto też sprawdzić czy po przekonwertowaniu pliku MainAcitivity czy jak nazwaliśmy główną aktywność dodało się tam w tym samym build.gradle oprócz linijki:

apply plugin: ‘kotlin-android’

linijka:

apply plugin: ‘kotlin-android-extensions’

Umożliwia ona czytelne odwoływanie się do elementów layoutu.

Skąd pobierzemy informację o pogodzie?

Z pomocą przychodzi nam serwis yahoo
Gdzie zaznaczamy one world weather condition:
yahoo weather api
Widzimy też jak wygląda odpowiedź serwera na nasze zapytanie wysłane na ten konkretny adres URL. Kopiujemy sobie adres URL podany na stronie yahoo. Oczywiście podmienimy wartość wstawioną przez producenta tak aby użytkownik aplikacji mógł pobrać sobie dane pogodowe dowolnego miasta. (dostępnego przez yahoo weather)

Czas na kod

Przechodzimy do pliku z naszą główną aktywnością. Powinniśmy zastać taką domyślną klasę:

class MainActivity : AppCompatActivity() {

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

Więc dopisujemy następną funkcję możemy ją nazwać oczywiście dowolnie ale ta nazwa musi się zgadzać z nazwą w onClick naszego przycisku. Ja ja nazwę getWeatherCondition(view: View). Ważne jest, że jako argument funkcja musi przyjąć argument widoku. Innaczej nasza aplikacja dostanie błąd: AndroidRuntime: FATAL EXCEPTION
Czyli funkcja wygląda tak:

fun getWeatherCondition(view: View) {

}

A fragment kodu w Buttonie tak:

android:onClick="getWeatherCondition"

jeśli zaznaczymy tą nazwę i klikniemy SHIFT + F6 to możemy ją edytować i IDE podmieni nową nazwę w całym projekcie. Bardzo przydatna funkcjonalność.

Następnie pobierzemy miasto z naszego pola EditText

Pobieranie takiej wartości jest bardzo proste. Tworzymy stałą city:

fun getWeatherCondition(view: View) {
    val city = etCityName.text.toString()
}

Gdzie etCityName to id naszego EditText. I Wpisujemy tę wartość w URL. Oczywiście musimy też dodać URL. Pamiętając o tym, że w kotlinie korzystamy ze znaku dolara $.

fun getWeatherCondition(view: View) {
    val city = etCityName.text.toString()
    val oneWordURL = "https://query.yahooapis.com/v1/public/yql?q=select%20item.condition.text%20from%20weather.forecast%20where%20woeid%20in%20(" +
                "select%20woeid%20from%20geo.places(1)%20where%20text%3D%22<strong>$city</strong>%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys"
}

Teraz w końcu użyjemy obiecanych bibliotek. Jednak sprawa dzięki tym biblioteką jest bardzo prosta! By wysłać zapytanie na adres URL musimy wykonać to zadanie na innym wątku, przecież nie chcemy zawieszać telefonu po to aby aplikacja połączyła się z jakąś stroną internetową. A wiemy, że przy słabym zasiegu, złej pogodzie, przeciążeniu serwera etc. takie zapytanie może trwać długo i być nieznośne dla użytkownika. Dlatego korzystamy z wielowątkowości.
Dzięki Anko jest to dziecinie proste.

doAsync {

}

Następnie łączymy się z wybranym adresem url. I Czytamy go jako tekst. W Javie trzebabyłoby napisać funckję pobierającą strumień i konwertującą go na stringa. Standardowe rozwiązanie, proste. Jednak wymaga czasu i napisania go. A jednoczesie jest to standradowe zachowanie przy czytaniu stron więc teraz mozemy to uprosić w przypadku małych stron, niedużych JSONów. Metodą .readText()

val url = URL(oneWordURL).readText()

Teraz odpowiedź w JSONie którą trzymamy w zmiennej url typu String bo tak nam zwraca metoda readText() co jest logiczne. Parsujemy. Czyli zamieniami JSONa na POJO o którym już pisałem. Całość możemy podstawić do jednej zmiennej.

val result = parseJson(URL(oneWordURL).readText())

Pozostaje zaimplementować metodę parseJson(json: String) bo jako arugment przyjmuje:

val url = URL(oneWordURL).readText()

czyli wartość String. Pod tym linkiem znaduje się przykładowy JSON. który wygląda tak:
JSON
Więc musimy stworzyć odpowiadającą temu JSONowi klasę. Nazwiemy ją odpowiedź. Tworzymy nowy pilk, klasę kotlin. np. Query.kt
Całość traktujemy jako odpowiedź. Tak to widzimy, więc pierwsza nazwa klasy jest bez znaczenia. Nazwijmy ją Response.

class Response(val query: Query)

Odpowiedź jako arugment przyjmuje query Typu Query, który musimy zdefiniować. Ale nie sprawia to żadnej trudności to kolejne obiekty JSON.

class Query(val count: Int, val created: String, val lang: String, val results: Results)

Możemy tak naprawdę podać jako argument tylko to co nasz interesuje czyli results. Bo tam mocno zagnieżdżona -> znajduję się informacja o pogodzie. Alę podaję wszystko dla jasności. Kolejną ważną sprawą jest to, że nazwy argumentów muszą odpowiadać nazwą w JSONie. innaczej muslibyśmu użyć adnotacji. A po co skoro tak jest czytelniej?
KOlejno widzimy Result, które ma w sobie Channel itd. aż do Condition i pola Text typu String. Czyli całość wygląda tak:

class Response(val query: Query)
class Query(val count: Int, val created: String, val lang: String, val results: Results)
class Results(val channel: Channel)
class Channel(val item: Item)
class Item(val condition: Condition)
class Condition(val text: String)

Ok, mamy konstrukcję do sparsownania odpowiedzi JSON jako obiekt POJO. Teraz musimy z niego wydostać to co chcemy i dokończyć funkcję fun parseJson(json: String){…}.
Ta funkcja jest dość prosta. Korzystamy z GsonBuildera, którego trzeba stworzyc i z Jsona Tworzymy sobie POJO. Co wygląda tak:

private fun parseJson(json: String) = GsonBuilder()
            .create()
            .fromJson(json, Response::class.java)
            .query.results.channel.item.condition.text

Na końcu od razu odwołuje się do .query.results.channel.item.condition.text tego co nas interesuje czyli jedno słownym opisem pogody.
Pozostaje zaktualizować TextView. Aby tego dokonać w doAsync {} musimy powiedzieć, na którym wątku chcemy tego dokonać. Pamietajmy, ze nie blokujemy UserInterface naszym zapytaniem do serwera yahoo ale teraz chcemy zaaktualizować ten UI. Czyli piszemy:

uiThread {
    updateTextView(result)
}

Pozostaje jeszcze napisać funckję updateTextView która jako argument przyjmuje result które jest Stringiem z opisem pogody.
Tu sprawa również jest bardzo prosta:

private fun updateTextView(s: String) {
        tv.text = "One word weather condition $s"
    }

tv – to ID naszego TextView.
Całość dostępna na githubie. Zachęcam do skomentowania. Jeśli coś jest nie jasne zapraszam do kontaktu: adriankujawski@kotliners.pl Masz pomysł na aplikację, tutorial, serię? Napisz co chcesz zobaczyć a postaram się to przygotować. Mailowo lub w komentarzach. Postaram się odpowiedzieć jak najszybciej. 🙂

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *