Richtig aufregend ist unsere App noch nicht, denn sie ist nicht dynamisch genug - es muss ordentlicher Content her!

Deswegen implementieren wir in dieser Session eine Anbindung an eine Web-API. Über diese bekommen wir hochdynamischen, immer aktuellen Content.

Wir nutzen sogenannte Repositories um eine Abstraktionsschicht einzuführen, die bestimmt wo genau Daten herkommen.
Man kann das oft als Interface Implementieren, wobei dann öffentliche Methoden oft nur getter darstellen. Z.B. führt dann eine getProfile()-Methode zum Ergebnis eines Profile. Die interne implementierung kann dieses aus einer Datenbank holen oder auch über einen Webservice; für die Benutzung ist das aber völlig irrelevant wo es herkommt. Zusätzlich hat dieses Vorgehen den Vorteil, dass die eigentliche Implementierung ausgetauscht werden kann. So könnte das Repository zu Beginn der Entwicklung noch ein gemocktesProfile laden, das der Entwickler hard gecoded vorgibt und später durch das "echte" Laden des Profils austauscht.

Für unsere App nutzen wir eine beliebte API bei der wir uns relativ sicher gehen können, dass sie erreichbar ist. Daher darf hier GitHub für uns herhalten.

Typischerweise bieten solche APIs eine REST-like Schnittstelle an, die per JSON-Objekte Informationen austauscht. Quasi-Industriestandard ist für die Konsumierung auf der Androidseite Retrofit. Diese Library benötigt ein wenig Einarbeitung ist aber recht schlank und trotzdem sehr mächtig.

Damit wir in unserer App sofort damit anfangen können, ist eine kleine Anbindung bereits vorgefertigt im nächsten Abschnitt zu finden.

Damit wir überhaupt loslegen können, müssen wir im Android Manifest sicher gehen, dass auch die Permission für das Internet gesetzt ist - das haben wir aber dankender Weise schon im letzten Schritt getan.
Ansonsten brauchen wir noch
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
um den Status des Netzwerkes abzufragen - das ist ein cooles, aber nicht immer notwendiges Feature.

Wir brauchen jetzt noch ein paar Dependencies zu ein paar Libraries - zu diesen selbst gibt es nachher mehr zu lesen. Lege also wie gewohnt im gradle.build File des App Moduls:

// networking
implementation "com.squareup.retrofit2:retrofit:2.5.0"
implementation "com.squareup.okhttp3:logging-interceptor:3.12.2"
implementation "com.squareup.okhttp3:okhttp-urlconnection:3.11.0"
implementation "com.squareup.okhttp3:okhttp:3.12.2"
implementation "com.squareup.retrofit2:adapter-rxjava2:2.5.0"
implementation "com.squareup.retrofit2:converter-gson:2.5.0"
// reactive
implementation "io.reactivex.rxjava2:rxjava:2.2.6"
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
implementation "io.reactivex.rxjava2:rxkotlin:2.3.0"

Jetzt lege ein Kotlin-File an namens BootcampRepository (bei com.example.helloworld). Kopiere hier den kompletten Inhalt:

import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.util.Log
import androidx.annotation.Keep
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import io.reactivex.rxkotlin.addTo
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
import android.content.Intent
import android.content.BroadcastReceiver
import android.content.IntentFilter
import android.os.Build
import io.reactivex.*
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import retrofit2.http.*


/**
 * A repository for accessing a public api.
 *
 * This is a 'one has it all'-file, optimized for showcasing and simple usage. In a productive environment, these
 * components should have higher abstractions; the repository implementation for example is better exchangeable if put
 * behind an interface.
 *
 * @author Thomas Hofmann
 */
object BootcampRepository {

    /**
     * Listens for network events, like [State.LOADING] and alike.
     *
     * **Note:**
     * A productive app would not really listen once and only have one listener here for all network events.
     *
     * A system for registering the state for every call is better suited.
     * Using this technique here will have a wrong behaviour when having multiple requests at once, as the first
     * request finishing or failing will fire an unassociated event (ask yourself: what should the UI display then?).
     * For a simple usage tutorial on the other hand, it is a nice little listener.
     */
    var networkListener: ((NetworkState) -> Unit)? = null

    /**
     * Initialize the repository. Has to be done before making the first call.
     *
     * **Note and be cautious:**
     * Typically such methods are placed in the application-object on startup, as this has to be done only once.
     * In this showcase, when added to a generic context, the context gets leaked at some point, because we  are not
     * getting rid of hard references, for example set through the disposable and the registered receivers! This is a
     * memory leak and negatively affects performance. Never do this in productive apps!
     */
    fun init(context: Context, apiKey: String? = null) {
        apiKey?.let { this.apiKey = it } // example for adding an api key, not needed here but it showcases one

        // supporting legacy and new network detection
        if (Build.VERSION.SDK_INT < 24) {
            val intentFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
            context.registerReceiver(LegacyConnectivityStatusReceiver(), intentFilter)
            return
        }

        val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
        connectivityManager?.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                networkListener?.invoke(NetworkState(Status.AVAILABLE))
            }

            override fun onLost(network: Network?) {
                networkListener?.invoke(NetworkState(Status.LOST))
            }
        })
    }

    /**
     * A legacy broadcast receiver to be notified about network state.
     */
    class LegacyConnectivityStatusReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager

            manager?.activeNetworkInfo?.let {
                if (it.isConnected) {
                    networkListener?.invoke(NetworkState(Status.AVAILABLE))
                }
                return
            }

            networkListener?.invoke(NetworkState(Status.LOST))
        }
    }

    // friendly reminder:
    // a productive app should not have
    // this here, as it is not really disposable in this singleton!
    private val compositeDisposable = CompositeDisposable()

    private var apiKey = ""

    private val api: Api by lazy {
        buildApi()
    }

    private fun buildApi(): Api {
        val retrofit = Retrofit.Builder()
            .client(buildOkHttpClient())
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync())
            .baseUrl("https://api.github.com/")
            .build()

        return retrofit.create(Api::class.java)
    }

    private fun buildOkHttpClient() = OkHttpClient
        .Builder()
        .addInterceptor(HttpLoggingInterceptor(HttpLoggingInterceptor.Logger {
            if (BuildConfig.DEBUG) {
                Log.d("API", it) // logs the output of the api consumption to logcat
            }
        }).apply {
            level = HttpLoggingInterceptor.Level.BODY
        })
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build()

    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    //                                                                                                                //
    // Requests                                                                                                       //
    //                                                                                                                //
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Requests loading of games.
     */
    fun getUsers(since: String = "1", callback: (List<UserResponse>?) -> Unit) {
        api.getUsers(since)
            .with(AndroidSchedulerProvider())
            .subscribeToCompletion(callback)
            .addTo(compositeDisposable)
            .run { networkListener?.invoke(NetworkState(Status.LOADING)) }
    }

    private fun <T, E : Response<T>> Single<E>.subscribeToCompletion(
        onComplete: (T) -> Unit,
        onError: (Throwable) -> Unit = {}
    ): Disposable {
        return doOnSuccess {
            networkListener?.invoke(NetworkState(Status.IDLE))
        }.doOnError { error ->
            networkListener?.invoke(NetworkState(Status.ERROR, error.localizedMessage ?: ""))
        }.subscribe({ it?.body()?.let(onComplete) }, onError)
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//                                                                                                                    //
// Api interface                                                                                                      //
//                                                                                                                    //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

interface Api {

    @Headers("Accept: application/json")
    @GET("/users")
    fun getUsers(@Query("since") since: String): Single<Response<List<UserResponse>?>>
}

/**
 * Lists all possible network statuses.
 */
enum class Status {

    /**
     * Idle state, if nothing has to be done or is processed right now.
     */
    IDLE,

    /**
     * Loading state, when something is loading right now.
     */
    LOADING,

    /**
     * Error state when something fails. Try to fetch the info message then.
     */
    ERROR,

    /**
     * Wehen network is available.
     */
    AVAILABLE,

    /**
     * When network connection has been lost.
     */
    LOST
}

/**
 * A network state containing the state identifier, one of [NetworkState]. May have useful additional [info].
 */
data class NetworkState(val status: Status, val info: String = "")

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//                                                                                                                    //
// Api models                                                                                                         //
//                                                                                                                    //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

@Keep
data class UserResponse(
    val login: String,
    val id: Int,
    val avatar_url: String
)

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//                                                                                                                    //
// Reactive Utils                                                                                                     //
//                                                                                                                    //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

//
// Extension functions for less reactive boilerplate code
//

fun Completable.with(schedulerProvider: SchedulerProvider): Completable =
    this.observeOn(schedulerProvider.ui()).subscribeOn(schedulerProvider.io())

fun <T> Single<T>.with(schedulerProvider: SchedulerProvider): Single<T> =
    this.observeOn(schedulerProvider.ui()).subscribeOn(schedulerProvider.io())

fun <T> Observable<T>.with(schedulerProvider: SchedulerProvider): Observable<T> =
    this.observeOn(schedulerProvider.ui()).subscribeOn(schedulerProvider.io())

fun <T> Flowable<T>.with(schedulerProvider: SchedulerProvider): Flowable<T> =
    this.observeOn(schedulerProvider.ui()).subscribeOn(schedulerProvider.io())

fun <T> Maybe<T>.with(schedulerProvider: SchedulerProvider): Maybe<T> =
    this.observeOn(schedulerProvider.ui()).subscribeOn(schedulerProvider.io())

fun Disposable.start(compositeDisposable: CompositeDisposable, start: () -> Unit = {}): Disposable {
    return apply {
        compositeDisposable.add(this)
        start()
    }
}

//
// Scheduler abstractions
//

interface SchedulerProvider {
    fun io(): Scheduler
    fun ui(): Scheduler
    fun computation(): Scheduler
}

class AndroidSchedulerProvider : SchedulerProvider {
    override fun io() = Schedulers.io()
    override fun ui() = AndroidSchedulers.mainThread()
    override fun computation() = Schedulers.computation()
}

Hier passiert eine Menge: Ein HttpClient wird gebaut und verbunden mit der API von Github. An sich ist hier vieles Code, den das Framework Retrofit braucht um diese Verbindung herzustellen und ein Mix aus dem Repository, das das Interface nach außen gibt und ein paar Hilfsfunktionen, um intern Retrofit per RxJava zu benutzen - wie das genau funktioniert, ist aber für unsere Beispielanwendung erstmal nicht wichtig. Es zählen hier die öffentlichen Methoden des Repositories, also init() und getUsers().

Das Projekt sollte erfolgreich bauen - aber natürlich noch nichts herunterladen, das passiert in der nächsten Übung.

In dieser Übung versuchen wir nun mit der neuen Anbindung etwas sinnvolles zu machen.
Ziel ist es die Liste, die wir in der Session zuvor angelegt haben mit den Daten aus dem Web zu füllen. Dafür müssen wir einmal das Repository im ListFragment initialisieren. Entferne also listAdapter.setItems(...) und ersetzte es mit

BootcampRepository.init(requireContext())

BootcampRepository.getUsers { loadedUsers ->

}

Wie du hier sehen kannst, wird auch gleich der Netzwerk-Call mit "getUsers" ausgeführt. In users ist also eventuell eine Liste enthaltent. Bedenke aber:

Wir können den Adapter also gar nicht befüllen ?!

Wie dir vermutlich aufgefallen ist, kann die Liste nicht einfach mit setItems() mit der Liste des Netzwerkcalls gefüllt werden - die Datentypen unterscheiden sich.
Jetzt gibt es mindestens 2 Möglichkeiten, die man in Betracht ziehen sollte, um das Problem zu beheben:

  1. Der Adapter der Liste nimmt den Datentypen der Liste entgegen, nicht den User. Hier muss ein wenig refactored werden
  2. Es gibt ein Mapping vom Netzwerk Datentypen zu dem des Adapters. Das geht super einfach in der obigen Methode.

Da die zweite Lösung wohl die in Kotlin spannendere ist, versuche doch mal Lösung 2!

Schreibe in dieser Arbeit also ein Mapping das von dem Datentyp des Requests in den User Datentyp der ersten Arbeit umwandelt und den Adapter damit befüllt.

Wie dir vielleicht aufgefallen ist, ist der Call für die Repos asynchron. Das heißt an sich wissen wir nicht wann und ob überhaupt der Call zu einem Ergebnis führt und wie lange das dauert.
Da wir als Appnutzer aber in der Regel nicht lange warten wollen, ohne irgend ein Form von Feedback zu bekommen, ist es von besonderer Bedeutung, während dem Ladevorgang einen Hinweis zu geben, dass gerade ein Request im Gange ist.

Daher fügen wir im Layout der Liste eine Progressbar ein, mit

    <ProgressBar
        android:id="@+id/loadingProgress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true" />

Lass dich von dem Namen nicht irritieren, eine Progressbar ist in dieser Form der "Ladekringel" so wie man ihn kennt, und keine horizontale "Bar".

Nutze nun die Methoden, die das Repository bietet um den Ladekringel anzuzeigen, wenn es lädt und wieder zu verstecken, wenn es geladen wurde.

Tipp:
Nutze

loadingProgress.visibility = (View.VISIBLE)

und

loadingProgress.visibility = (View.GONE)

für das Umschalten der Anzeige und denke daran, dass sie so wie sie oben definiert ist, erstmal angezeigt wird, auch wenn noch nichts geladen wird, das macht vielleicht nicht so viel Sinn!

Die App kann jetzt schon einiges.

Als Nächstes kommt noch ein wenig Feintuning, dann ein paar Considerations, wo man wie weiter machen könnte und dann sind wir erstmal fertig!

Mache nun weiter mit dem Polishing.