= Android / Kotlin Lecture Notes
:author: Thomas W. Stütz
:revnumber: 1.0.0
:revdate: {docdate}
:revremark: NVS 4th and 5th grade HTL Leonding Vocational College
:encoding: utf-8
:experimental:
ifndef::imagesdir[:imagesdir: images]
//:toc-placement!:  // prevents the generation of the doc at this position, so it can be printed afterwards
:source-highlighter: rouge
:sourcedir: ../src/main/java
:icons: font
:sectnums:    // Nummerierung der Überschriften / section numbering
:toc: left
:toclevels: 5  // this instructions MUST set after :toc:
:linkattr:  // to be sure to process ", window="_blank""

//Need this blank line after ifdef, don't know why...
ifdef::backend-html5[]

// https://fontawesome.com/v4.7.0/icons/
icon:file-text-o[link=https://raw.githubusercontent.com/htl-leonding-college/android-classroom-course/main/asciidocs/{docname}.adoc] ‏ ‏ ‎
icon:github-square[link=https://github.com/htl-leonding-college/android-classroom-course] ‏ ‏ ‎
icon:home[link=https://htl-leonding-college.github.io/android-classroom-course]
endif::backend-html5[]

// print the toc here (not at the default position)
//toc::[]


== Mealz App

* excerpt from https://www.udemy.com/course/jetpack-compose-masterclass

=== Update Dependencies

=== MVVM

image::mealz/mvc.png[]

image::mealz/mvp-mvvm.png[]

image::mealz/mvvm.png[]

image::mealz/aac-viewmodel.png[]




=== Create ViewModel

.build.gradle (Module)
[source,groovy]
----
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
----

.MealsCategoriesViewModel.kt
[source,kotlin]
----
package at.htl.mealzapp.ui.meals

import androidx.lifecycle.ViewModel
import at.htl.model.MealsRepository
import at.htl.model.response.MealResponse

class MealsCategoriesViewModel(
    private val repository: MealsRepository = MealsRepository()
) : ViewModel() {

    fun getMeals(): List<MealResponse> {
        return repository.getMeals().categories
    }

}
----


.MainActivity
[source,kotlin]
----
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MealzAppTheme {
                MealsCategoriesScreen()
            }
        }
    }
}

@Composable
fun MealsCategoriesScreen() {
    val viewModel : MealsCategoriesViewModel = viewModel() // <.>
    Text(text = "Hello Tom!")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MealzAppTheme {
        MealsCategoriesScreen()
    }
}
----

<.> The viewModel will NOT reinstantiated by every compose cycle, it will live as long the screen (the composable, activity or fragment) will live.



=== The MealsDB

* https://www.themealdb.com/api.php
* https://www.themealdb.com/api/json/v1/1/categories.php


=== JSON

image::mealz/json-response.png[]

=== GSON deserialization

* JSON -> data classes
* we use gson
* an valid alternative is https://github.com/square/moshi[moshi^]

.build.gradle (Module)
[source,groovy]
----
// Retrofit
implementation 'com.google.code.gson:gson:2.10'
----

.MealResponse.kt
[source,kotlin]
----
package at.htl.model.response

import com.google.gson.annotations.SerializedName

data class MealsCategoriesResponse(
    val categories: List<MealResponse>
) {
}

data class MealResponse(
    @SerializedName("idCategory") val id: String,
    @SerializedName("strCategory") val name: String,
    @SerializedName("strCategoryDescription") val description: String,
    @SerializedName("strCategoryThumb") val imageUrl: String
)
----

.MealsRepository.kt
[source,kotlin]
----
package at.htl.model

import at.htl.model.response.MealsCategoriesResponse

class MealsRepository {

    fun getMeals(): MealsCategoriesResponse = MealsCategoriesResponse(arrayListOf())

}
----

=== Retrofit

* now we use retrofit to get the data from the rest api and furthermore to convert them into objects
* https://square.github.io/retrofit/

[source,groovy]
----
// Retrofit
implementation 'com.google.code.gson:gson:2.10'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
----

.manifests/AndroidManifests.xml
[source,xml]
----
<uses-permission android:name="android.permission.INTERNET" />
----

.MealsWebService.kt
[source,kotlin]
----
package at.htl.model.api

import at.htl.model.response.MealsCategoriesResponse
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET

class MealsWebService {

    private lateinit var api: MealsApi

    init {
        val retrofit = Retrofit.Builder()
            .baseUrl("https://www.themealdb.com/api/json/v1/1/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        api = retrofit.create(MealsApi::class.java)
    }


    fun getMeals(): Call<MealsCategoriesResponse> {
        return api.getMeals()
    }

    interface MealsApi {
        @GET("categories.php")
        fun getMeals(): Call<MealsCategoriesResponse>
    }

}
----




.MealsRepository.kt
[source,kotlin]
----
package at.htl.model

import at.htl.model.api.MealsWebService
import at.htl.model.response.MealsCategoriesResponse
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class MealsRepository(
    private val webService: MealsWebService = MealsWebService()
) {
    fun getMeals(
        successCallback: (response: MealsCategoriesResponse?) -> Unit
    ) {
        return webService.getMeals().enqueue(object : Callback<MealsCategoriesResponse> {
            override fun onResponse(
                call: Call<MealsCategoriesResponse>,
                response: Response<MealsCategoriesResponse>
            ) {
                if (response.isSuccessful)
                    successCallback(response.body())
            }

            override fun onFailure(call: Call<MealsCategoriesResponse>, t: Throwable) {

            }
        })
    }
}
----




.MealsCategoriesViewModel.kt
[source,kotlin]
----
package at.htl.mealzapp.ui.meals

import androidx.lifecycle.ViewModel
import at.htl.model.MealsRepository
import at.htl.model.response.MealsCategoriesResponse

class MealsCategoriesViewModel(
    private val repository: MealsRepository = MealsRepository()
) : ViewModel() {

    fun getMeals(
        successCallback: (response: MealsCategoriesResponse?) -> Unit
    ) {
        repository.getMeals() { response ->
            successCallback(response)
        }
    }
}
----




.MainActivity.kt
[source,kotlin]
----
//...

@Composable
fun MealsCategoriesScreen() {
    val viewModel: MealsCategoriesViewModel = viewModel()
    val rememberedMeals: MutableState<List<MealResponse>> = remember {
        mutableStateOf((emptyList<MealResponse>()))
    }
    viewModel.getMeals { response ->
        val mealsFromTheApi = response?.categories
        rememberedMeals.value = mealsFromTheApi.orEmpty()
    }
    LazyColumn {
        items(rememberedMeals.value) { meal ->
            Text(text = meal.name)
        }

    }
}

//...
----


.manifest.xml
[source,xml]
----
<uses-permission android:name="android.permission.INTERNET" />
----

=== Coroutines

image::mealz/coroutines1.png[]

image::mealz/coroutines2.png[]

image::mealz/coroutines3.png[]

image::mealz/coroutines4.png[]





.MealsWebService.kt
[source,kotlin]
----
class MealsWebService {

    private lateinit var api: MealsApi

    // ...

    suspend fun getMeals(): MealsCategoriesResponse { // <.>
        return api.getMeals()
    }

    interface MealsApi {
        @GET("categories.php")
        suspend fun getMeals(): MealsCategoriesResponse // <.>
    }

}
----

<.> convert to suspend function
<.> convert to suspend function


.MealsRepository.kt
[source,kotlin]
----
class MealsRepository(
    private val webService: MealsWebService = MealsWebService()
) {

    suspend fun getMeals(): MealsCategoriesResponse { // <.>
        return webService.getMeals()
    }

}
----

<.> convert to suspend function


.MealsCategoriesViewModel.kt
[source,kotlin]
----
class MealsCategoriesViewModel(
    private val repository: MealsRepository = MealsRepository()
) : ViewModel() {

    suspend fun getMeals(): List<MealResponse> {
        return repository.getMeals().categories
    }
}
----




.MainActivity.kt
[source,kotlin]
----
// ...

@Composable
fun MealsCategoriesScreen() {
    val viewModel: MealsCategoriesViewModel = viewModel()
    val rememberedMeals: MutableState<List<MealResponse>> = remember {
        mutableStateOf((emptyList<MealResponse>()))
    }
    val coroutineScope = rememberCoroutineScope()  // <.>

    LaunchedEffect(key1 = "GET_MEALS") {  // <.>
        coroutineScope.launch(Dispatchers.IO) {
            val meals = viewModel.getMeals()
            rememberedMeals.value = meals
        }
    }

    LazyColumn {
        items(rememberedMeals.value) { meal ->
            Text(text = meal.name)
        }

    }
}

// ...
----

<.> get the corutine scope
<.> use LaunchedEffects, so the coroutine will be startet once and not at every composition


=== Hoisting state and Coroutines

* We don't want to trigger the request for the meals in the compose function. We will transfer it to the ViewModel.

.MealsCategoriesViewModel.kt
[source,kotlin]
----
class MealsCategoriesViewModel(
    private val repository: MealsRepository = MealsRepository()
) : ViewModel() {

    private val mealsJob = Job()   // <.>
    init {
        val scope = CoroutineScope(mealsJob + Dispatchers.IO)
        scope.launch() {  // <.>
            val meals = getMeals()
            mealsState.value = meals
        }
    }

    val mealsState: MutableState<List<MealResponse>> = mutableStateOf((emptyList<MealResponse>()))

    override fun onCleared() {
        super.onCleared()
        mealsJob.cancel()  // <.>
    }

    private suspend fun getMeals(): List<MealResponse> {
        return repository.getMeals().categories
    }
}
----

<.> we create our own scope, even thats not necessary, because we could use the ViewModel-scope
<.> we launch the scope once, when the ViewModel is created
<.> we override a method, so the coroutine will be cancelled, when the ViewModel is destroyed.


.MainActivity.kt
[source,kotlin]
----
@Composable
fun MealsCategoriesScreen() {
    val viewModel: MealsCategoriesViewModel = viewModel()
    val coroutineScope = rememberCoroutineScope()
    val meals = viewModel.mealsState.value   // <.>

    LazyColumn {
        items(meals) { meal ->
            Text(text = meal.name)
        }
    }
}
----

<.> here we create the ViewModel

=== Use the ViewModel-Scope


.MealsCategoriesViewModel.kt
[source,kotlin]
----
class MealsCategoriesViewModel(
    private val repository: MealsRepository = MealsRepository()
) : ViewModel() {

    val TAG = MealsCategoriesViewModel::class.java.name

    init {
        Log.d(TAG, "we are about to launch a coroutine")
        viewModelScope.launch(Dispatchers.IO) {  // <.>
            Log.d(TAG, "we have launched the coroutine")
            val meals = getMeals()
            Log.d(TAG, "we have received the asynchronous data")
            mealsState.value = meals
        }
        Log.d(TAG, "other work")
    }

    val mealsState: MutableState<List<MealResponse>> = mutableStateOf((emptyList<MealResponse>()))

    // <.>

    private suspend fun getMeals(): List<MealResponse> {
        return repository.getMeals().categories
    }
}
----

<.> we only use `viewModelScope.launch(Dispatchers.IO) { ... }`

<.> we do not to override `onCleared()` because it is already implemented with the ViewModel-scope


image::mealz/coroutines5.png[]

image::mealz/coroutines6-logcat.png[]


=== Display Image with Coil and Create Screen-Class

* https://coil-kt.github.io/coil/compose/[^]


image::mealz/project-structure.png[]

.MealsCategoriesScreen.kt
[source,kotlin]
----
package at.htl.mealzapp.ui.meals

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import at.htl.mealzapp.ui.theme.MealzAppTheme
import at.htl.model.response.MealResponse
import coil.compose.AsyncImage

@Composable
fun MealsCategoriesScreen() {
    val viewModel: MealsCategoriesViewModel = viewModel()
    val meals = viewModel.mealsState.value

    LazyColumn(contentPadding = PaddingValues(16.dp)) {
        items(meals) { meal ->
            MealCategory(meal)
        }
    }
}

@Composable
fun MealCategory(meal: MealResponse) {
    Card(
        shape = RoundedCornerShape(8.dp),
        elevation = 2.dp,
        modifier = Modifier
            .fillMaxWidth()
            .padding(top = 16.dp)
    ) {
        Row {
            AsyncImage(
                model = meal.imageUrl,
                contentDescription = null,
                modifier = Modifier
                    .size(88.dp)
                    .padding(4.dp)
            )
            Column(
                modifier = Modifier
                    .align(Alignment.CenterVertically)
                    .padding(16.dp)
            ) {
                Text(text = meal.name)
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MealzAppTheme {
        MealsCategoriesScreen()
    }
}
----





.MainActivity.kt
[source,kotlin]
----
package at.htl.mealzapp.ui

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import at.htl.mealzapp.ui.meals.MealsCategoriesScreen
import at.htl.mealzapp.ui.theme.MealzAppTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MealzAppTheme {
                MealsCategoriesScreen()
            }
        }
    }
}
----



































































[source,kotlin]
----

----









