= Android Lab: Reactive App with Java
Thomas W. Stütz
0.0.1, 2024-05-08 : Android - ToDo App
ifndef::imagesdir[:imagesdir: images]
//:toc-placement!: // prevents the generation of the doc at this position, so it can be printed afterwards
:sourcedir: ../src/main/java
:icons: font
:sectnums: // Nummerierung der Überschriften / section numbering
:toc: left
:toclevels: 5
:experimental:
// https://mrhaki.blogspot.com/2014/06/awesome-asciidoc-use-link-attributes.html
:linkattrs:
//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-reactive-java-todo/main/asciidocs/docs/{docname}.adoc]
icon:github-square[link=https://github.com/htl-leonding-college/android-reactive-java-todo]
icon:home[link=http://edufs.edu.htl-leonding.ac.at/~t.stuetz/hugo/]
endif::backend-html5[]
// print the toc here (not at the default position)
toc::[]
== Prerequisites - What students have to prepare
* install Android Studio
== Reactive in Android
* https://github.com/ReactiveX/RxAndroid[RxAndroid: Reactive Extensions for Android^]
* https://github.com/ReactiveX/RxJava[RxJava^], https://github.com/ReactiveX/RxJava/wiki[Wiki^]
== Creating the project
image::create-project-001.png[]
image::create-project-002.png[]
=== Add RxJava Library, Dagger/Hilt, resteasy-client, jackson-databind, smallrye-config
* https://stackoverflow.com/a/78328837/9818338[Hilt dependency in toml file with KSP^]
* additional info here: https://medium.com/@duaaawan/hilt-for-android-a-beginners-guide-to-dependency-injection-7f9cadc5526b[Hilt For Android: A Beginner’s Guide to Dependency Injection^]
IMPORTANT: As kapt is in maintenance mode, we use KSP
.build.gradle.kts(:app)
[%collapsible]
====
[source,kotlin]
----
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.kotlinAndroidKsp)
alias(libs.plugins.hiltAndroid)
}
android {
namespace = "at.htl.todo"
compileSdk = 34
defaultConfig {
applicationId = "at.htl.todo"
minSdk = 30
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.13"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
excludes += "/META-INF/INDEX.LIST"
excludes += "/META-INF/DEPENDENCIES"
excludes += "/META-INF/LICENSE.md"
excludes += "/META-INF/NOTICE.md"
}
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
// RxJava
implementation (libs.rxjava)
implementation(libs.rxandroid)
implementation(libs.androidx.runtime.rxjava3)
// Hilt
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
// Jackson
implementation(libs.jackson.databind)
// Resteasy
implementation(libs.resteasy.client)
// SmallRye Config
//implementation("org.eclipse.microprofile.config:microprofile-config-api:3.1") // for application.properties config loader
implementation(libs.smallrye.config)
}
----
====
.build.gradle.kts(todo)
[%collapsible]
====
[source,kotlin]
----
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
alias(libs.plugins.hiltAndroid) apply false
alias(libs.plugins.kotlinAndroidKsp) apply false
}
----
====
.libs.versions.toml
[%collapsible]
====
[source,toml]
----
[versions]
agp = "8.4.0"
hiltVersion = "2.51.1"
jacksonDatabind = "2.17.1"
kotlin = "1.9.23"
coreKtx = "1.13.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
lifecycleRuntimeKtx = "2.7.0"
activityCompose = "1.9.0"
composeBom = "2024.05.00"
resteasyClient = "6.2.8.Final"
rxjavaVersion = "3.1.8"
rxandroid = "3.0.2"
runtimeRxjava3 = "1.6.7"
ksp = "1.9.23-1.0.20"
smallryeConfig = "3.8.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltVersion" }
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltVersion" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jacksonDatabind" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
resteasy-client = { module = "org.jboss.resteasy:resteasy-client", version.ref = "resteasyClient" }
rxjava = { module = "io.reactivex.rxjava3:rxjava", version.ref = "rxjavaVersion" }
rxandroid = { module = "io.reactivex.rxjava3:rxandroid", version.ref = "rxandroid" }
androidx-runtime-rxjava3 = { module = "androidx.compose.runtime:runtime-rxjava3", version.ref = "runtimeRxjava3" }
smallrye-config = { module = "io.smallrye.config:smallrye-config", version.ref = "smallryeConfig" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlinAndroidKsp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hiltAndroid = { id = "com.google.dagger.hilt.android", version.ref = "hiltVersion" }
----
====
== Change to Project-View
image::intellij-project-view.png[]
== Separate Java-Business-Classes and Kotlin-Compose-Classes
* For separating Java and Kotlin classes we use Hilt as CDI-framework.
=== Configure Hilt - Add Hilt Application Class
image::hilt-error-name-in-manifest.png[]
* We create a new class `TodoApplication.java` as application entry point.
* This class is now our https://developer.android.com/training/dependency-injection/hilt-android#application-class[application-level dependency container^].
image::hilt-application-container.png[]
.at.htl.todo.TodoApplication
[source,java]
----
package at.htl.todo;
import android.app.Application;
import javax.inject.Singleton;
import dagger.hilt.android.HiltAndroidApp;
@HiltAndroidApp
@Singleton
public class TodoApplication extends Application {
}
----
* To check, if it is working, we use the Android - Logger
.at.htl.todo.TodoApplication with logging
[source,java]
----
package at.htl.todo;
import android.app.Application;
import javax.inject.Singleton;
import dagger.hilt.android.HiltAndroidApp;
@HiltAndroidApp
@Singleton
public class TodoApplication extends Application {
static final String TAG = TodoApplication.class.getSimpleName(); // <.>
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "App started ..."); // <.>
}
}
----
<.> Declare always the Logging-Tag
<.> Use the logging
.Add Application-Class and Internet-Access to `manifest.xml`
[source,xml,highlight=6]
----
<.>
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Todo"
tools:targetApi="31">
----
<.> Add here the permission for internet access
<.> Add here the name of the Hilt Application Class
.View in Logcat
image::hilt-log-app-started.png[]
=== @AndroidEntryPoint and Hilt Bindings
* Once Hilt is set up in your Application class and an application-level component is available, Hilt can provide dependencies to other Android classes that have the @AndroidEntryPoint annotation.
* https://developer.android.com/training/dependency-injection/hilt-android#android-classes[Inject dependencies into Android classes^]
* https://developer.android.com/training/dependency-injection/hilt-android#define-bindings[Define Hilt bindings^]
.at.htl.todo.ui.layout.MainView
[source,kotlin]
----
package at.htl.todo.ui.layout
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.tooling.preview.Preview
import at.htl.todo.ui.theme.TodoTheme
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MainView {
@Inject // <.>
constructor(){}
fun buildContent(activity: ComponentActivity) {
activity.enableEdgeToEdge()
activity.setContent {
TodoTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
TodoTheme {
Greeting("Android")
}
}
----
<.> Constructor injection (there are other ways, if constructor injection is not possible).
This is constructor injection with a primary constructor
+
[source,kotlin]
----
@Singleton
class MainView @Inject constructor() {
//...
}
----
.at.htl.todo.MainActivity
[source,java]
----
package at.htl.todo;
import android.os.Bundle;
import androidx.activity.ComponentActivity;
import javax.inject.Inject;
import at.htl.todo.ui.layout.MainView;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class MainActivity extends ComponentActivity {
@Inject
MainView mainView; // <.>
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mainView.buildContent(this); // <.>
}
}
----
<.> Now it is possible to inject the Jetpack Compose view
<.> When calling the kotlin function for building the view, we have to pass the Context of the current activity.
image::app-hello-android.png[]
== Add the Util-Classes
* link:files/util.zip[Download these files]
image::utils-project-tree.png[]
=== Immer
// TODO: Fundamentals for working with immutable states (immer)
=== Mapper
// TODO: Fundamentals ObjectMapper
image::mapper-structure.png[]
.at.htl.todo.model.
[source,java]
----
package at.htl.todo.util.mapper;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.io.IOException;
/** A Mapper that maps types to their json representation and back.
* ... plus a convenient deep-clone function
* @param the Class that is mapped
*/
public class Mapper {
private Class extends T> clazz;
private ObjectMapper mapper;
public Mapper(Class extends T> clazz) {
this.clazz = clazz;
mapper = new ObjectMapper()
.configure(SerializationFeature.INDENT_OUTPUT, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); // records
}
public String toResource(T model) {
try {
return mapper.writeValueAsString(model);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public T fromResource(String json) {
T model = null;
try {
model = mapper.readValue(json.getBytes(), clazz);
} catch (IOException e) {
throw new RuntimeException(e);
}
return model;
}
/** deep clone an object by converting it to its json representation and back.
*
* @param thing the thing to clone, unchanged
* @return the deeply cloned thing
*/
public T clone(final T thing) {
return fromResource(toResource(thing));
}
}
----
=== MessageBodyWriter / MessageBodyReader
.source: https://www.hameister.org/JEE7_JAXRS2_MesssageBodyReaderWriterList.html[MessageBodyReader und MessageBodyWriter für List- JAX-RS 2.0^]
image::https://www.hameister.org/images/JEE7_JAXRS_items.png[]
* https://javadoc.io/doc/jakarta.ws.rs/jakarta.ws.rs-api/latest/jakarta.ws.rs/jakarta/ws/rs/ext/MessageBodyReader.html[javadoc: MessageBodyReader^]
* https://javadoc.io/doc/jakarta.ws.rs/jakarta.ws.rs-api/latest/jakarta.ws.rs/jakarta/ws/rs/ext/MessageBodyWriter.html[javadoc: MessageBodyWriter^]
* https://www.examclouds.com/java/web-services/jax-rs-entity-providers[JAX-RS Entity Providers^]
=== Store
// TODO: Fundamentals Reactive Programming
////
=== Add microprofile config
* As in Quarkus we use the https://mvnrepository.com/artifact/io.smallrye.config/smallrye-config/3.8.1[SmallRye Config - Library^] which is following the https://microprofile.io/specifications/microprofile-config/[MicroProfile Config^]
* In the utils we already have a class `ConfigModule.java` for configuring SmallRye config.
image::config-project-tree.png[]
* Now it is possible to config our application in an `application.properties`-file.
+
.resources/application.properties
[source,properties]
----
json.placeholder.baseurl=https://jsonplaceholder.typicode.com
----
* microprofile-config.properties is an empty file
////
=== Add Configuration with application.properties
* Because SmallRye Config - Library didn't work, we use the assets - folder
* First create the assets-folder with the `application.properties`-file
+
image::config-assets-folder-project-tree.png[]
+
.main/assets/application.properties
[source,properties]
----
json.placeholder.baseurl=https://jsonplaceholder.typicode.com
----
* Then create a java class
+
.at.htl.todo.util.Config
[source,java]
----
package at.htl.todo.util;
import android.content.Context;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class Config {
private static Properties properties;
public static void load(Context context) {
try {
InputStream inputStream = context.getAssets().open("application.properties");
properties = new Properties();
properties.load(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
public static String getProperty(String key) {
return properties.getProperty(key);
}
}
----
* Finally, use your configuration i.e. in the MainActivity.java
+
[source,java]
----
@AndroidEntryPoint
public class MainActivity extends ComponentActivity {
// ...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Config.load(this);
var base_url = Config.getProperty("json.placeholder.baseurl");
Log.i(TAG, "onCreate: " + base_url);
mainView.buildContent(this);
}
}
----
image::config-assets-logcat-entry.png[]
IMPORTANT: Because this always needs a context, it is not usable in context-free services.
* https://github.com/smallrye/smallrye-config/issues/1057[Retrieving a SmallRyeConfig instance fails on Android^]
== Add the Model and Store
* https://redux.js.org/understanding/thinking-in-redux/three-principles[Three Principles^]
* https://reactivex.io/intro.html[ReactiveX^]
* https://jsonplaceholder.typicode.com/todos[Todos-Endpoint^]
image::store-project-tree.png[]
.at.htl.todo.model.Todo
[source,java]
----
package at.htl.todo.model;
public class Todo {
public Long userId;
public Long id;
public String title;
public boolean completed;
public Todo() {
}
public Todo(Long userId, Long id, String title, boolean completed) {
this.userId = userId;
this.id = id;
this.title = title;
this.completed = completed;
}
}
----
.at.htl.todo.model.Model
[source,java]
----
package at.htl.todo.model;
import java.util.List;
public class Model {
public Todo[] todos = {
new Todo(1L, 1L, "Buy milk", true), // <.>
new Todo(2L, 2L, "Buy eggs", false),
new Todo(2L, 3L, "Buy bread", false)
};
}
----
<.> For now, we use static data until implementing the rest client
.at.htl.todo.model.ModelStore
[source,java]
----
package at.htl.todo.model;
import javax.inject.Inject;
import javax.inject.Singleton;
import at.htl.todo.util.store.Store;
@Singleton
public class ModelStore extends Store {
@Inject
ModelStore() {
super(Model.class, new Model());
}
public void setTodos(Todo[] todos) {
apply(model -> model.todos = todos);
}
}
----
.at.htl.todo.util.store.Store
[source,java]
----
package at.htl.todo.util.store;
import java.util.concurrent.CompletionException;
import java.util.function.Consumer;
import at.htl.todo.util.immer.Immer;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
public class Store {
public final BehaviorSubject pipe;
public final Immer immer;
protected Store(Class extends T> type, T initialState) {
try {
pipe = BehaviorSubject.createDefault(initialState);
immer = new Immer(type);
} catch (Exception e) {
throw new CompletionException(e);
}
}
public void apply(Consumer recipe) {
pipe.onNext(immer.produce(pipe.getValue(), recipe));
}
}
----
.MainView (partly)
[source,kotlin]
----
@Singleton
class MainView @Inject constructor() {
@Inject
lateinit var store: ModelStore
fun buildContent(activity: ComponentActivity) {
activity.enableEdgeToEdge()
activity.setContent {
val viewModel = store
.pipe
.observeOn(AndroidSchedulers.mainThread())
.subscribeAsState(initial = Model())
.value
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Todos(model = viewModel, modifier = Modifier.padding(all = 32.dp))
}
}
}
}
----
.at.htl.todo.ui.layout.MainView
[%collapsible]
====
[source,kotlin]
----
package at.htl.todo.ui.layout
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Checkbox
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rxjava3.subscribeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import at.htl.todo.model.Model
import at.htl.todo.model.ModelStore
import at.htl.todo.model.Todo
import at.htl.todo.ui.theme.TodoTheme
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MainView @Inject constructor() {
@Inject
lateinit var store: ModelStore
fun buildContent(activity: ComponentActivity) {
activity.enableEdgeToEdge()
activity.setContent {
val viewModel = store
.pipe
.observeOn(AndroidSchedulers.mainThread())
.subscribeAsState(initial = Model())
.value
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Todos(model = viewModel, modifier = Modifier.padding(all = 32.dp))
}
}
}
}
@Composable
fun Todos(model: Model, modifier: Modifier = Modifier) {
val todos = model.todos
LazyColumn(
modifier = modifier.padding(16.dp)
) {
items(todos.size) { index ->
TodoRow(todo = todos[index])
HorizontalDivider()
}
}
}
@Composable
fun TodoRow(todo: Todo) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = todo.title,
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = todo.id.toString(),
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.weight(1f))
Checkbox(
checked = todo.completed,
onCheckedChange = { /* Update the completed status of the todo item */ }
)
}
}
@Preview(showBackground = true)
@Composable
fun TodoPreview() {
val model = Model()
val todo = Todo()
todo.id = 1
todo.title = "First Todo"
model.todos = arrayOf(todo)
TodoTheme {
Todos(model)
}
}
----
====
image::store-app-started.png[]
== Add REST-Client
INFO: You could also use https://square.github.io/retrofit/[Retrofit^]
image::rest-client-project-tree.png[]
.at.htl.todo.model.Model
[source,java]
----
package at.htl.todo.model;
import java.util.List;
public class Model {
public Todo[] todos = new Todo[0];
}
----
.at.htl.todo.model.TodoClient
[source,java]
----
package at.htl.todo.model;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.MediaType;
@Path("/todos")
@Consumes(MediaType.APPLICATION_JSON)
public interface TodoClient {
@GET
Todo[] all();
}
----
.at.htl.todo.model.TodoService
[source,java]
----
package at.htl.todo.model;
import android.util.Log;
import java.util.concurrent.CompletableFuture;
import javax.inject.Inject;
import javax.inject.Singleton;
import at.htl.todo.util.resteasy.RestApiClientBuilder;
@Singleton
public class TodoService {
static final String TAG = TodoService.class.getSimpleName();
public static String JSON_PLACEHOLDER_BASE_URL = "https://jsonplaceholder.typicode.com";
public final TodoClient todoClient;
public final ModelStore store;
@Inject
TodoService(RestApiClientBuilder builder, ModelStore store) {
Log.i(TAG, "Creating TodoService with base url: " + JSON_PLACEHOLDER_BASE_URL);
todoClient = builder.build(TodoClient.class, JSON_PLACEHOLDER_BASE_URL);
this.store = store;
}
public void getAll() {
CompletableFuture
.supplyAsync(() -> todoClient.all())
.thenAccept(store::setTodos);
}
}
----
=== Add Exception Handling
* When the result of the access to the rest-endpoint is empty and there is no error in Logcat, it is recommended not to swallow the error message (you should NEVER swallow an error message).
.at.htl.todo.model.TodoService
[source,java]
----
public void getAll() {
CompletableFuture
.supplyAsync(() -> todoClient.all())
.thenAccept(store::setTodos)
.exceptionally((e) -> { // <.>
Log.e(TAG, "Error loading todos", e);
return null;
});
}
----
<.> add here the exception handling
image::internet-access-error.png[]
image::rest-client-app-started.png[]