Suche
Viel dahinter

Backend as a Service: ein neues Geschäftsfeld für die Cloud

Peter Hoffmann
©Shutterstock/ Maksim Kabakou

Das Aufsetzen eines eigenen Backends erfordert Kenntnisse in entsprechenden Technologien, Datenbanken, Programmiersprachen etc. Durch die notwendige Hardware, den langwierigen Entwicklungsprozess und die ständige Wartung und Überwachung des Systems wird dies oft zu einem kostenintensiven Unterfangen. Genau hier setzt Backend as a Service an. Hiermit lassen sich Entwicklungs-, Hosting- und Wartungskosten sparen.

Backend as a Service ist im Kern das schnelle und einfache Erstellen von Backends durch Frontend-Developer im Browser. Bei der Entwicklung moderner Apps mit einem Backend gibt es immer drei Kompetenzschichten: die Administrationsebene, die Ebene der Backend-Entwicklung und die der Frontend-Entwicklung. Backend as a Service gehört der Kategorie des Cloud Computings an. Vor diesen Dienstleistungen musste man sich einen Server für sein Backend mieten oder kaufen. Reichte dieser nicht mehr aus, war es notwendig, weitere Server anzumieten. Jeder Server musste verbunden, konfiguriert und gewartet werden.

Durch Infrastructure as a Service (IaaS) wird zumindest dieses Problem eliminiert, indem bedarfsabhängig virtuelle Infrastrukturkomponenten durch einen IT-Anbieter zur Verfügung gestellt werden [1]. Ein weiteres Abstraktionslevel bietet Platform as a Service (PaaS). Dabei wird durch den Cloud-Anbieter ein Framework bereitgestellt [2]. So muss der Anwender sich nicht selbst um die Aktualisierung kümmern und hat immer die aktuellste Version zur Verfügung. Trotz dieser Abstraktion muss der Code des Backends bei IaaS oder PaaS aber immer noch selbst geschrieben werden. Darunter fallen die Verbindung zu den Datenbanken, die Schnittstellen und das Rechtemanagement.

Backend as a Service möchte die Entwicklung nun um noch einen Schritt vereinfachen. Dies gelingt, indem Schnittstellen für CREATE, READ, UPDATE, DELETE und entsprechende Datenbankverbindungen automatisch generiert werden. Das Datenmodell wird über eine grafische Oberfläche „zusammengeklickt“ – Code zu schreiben ist nicht mehr notwendig. Bei etablierten BaaS-Anbietern ist es bei Bedarf aber auch möglich, Server Logic zu injizieren. SDKs für verschiedene Programmiersprachen reduzieren ebenfalls die Menge des zu schreibenden Codes im Frontend. Zusammenfassend bietet BaaS folgende Vorteile:

  • Vereinfachte Kommunikation durch standardisierte Schnittstellen (z. B. REST-API), d. h. mehr Zeit zum Entwickeln, zum Testen und zum Verbessern
  • Wartung der Server, Skalierung und Sicherheitstests werden einfacher, da dies in die Zuständigkeit des BaaS-Anbieters fällt, das bedeutet: bis zu 95 Prozent Kosteneinsparung
  • Cross-Plattform (SDKs für Android, iOS, JavaScript etc.)
  • Sicheres Hosting in einer deutschen Cloud

 Anhand von zwei Beispiel-Apps wird nun verdeutlicht, wie der Entwicklungsprozess mit Backend as a Service aussieht. Dabei wurde von folgendem Szenario ausgegangen: Ein Bäcker möchte seine Kunden über Angebote informieren. Dazu soll an alle Benutzer der App ein Bild und eine kurze Nachricht gesendet werden. Die App soll zunächst für Android entwickelt werden. Zur Erstellung des Backends wird „apiOmat“ benuzt. Bei diesem Anbieter wird Push über Google Cloud Messaging (GCM) oder über Apple Push Notification Service neben der Möglichkeit, Bilder hochzuladen, angeboten. Die hier gezeigte App kann man mit dem Basic-Paket (kostenlos) nachbauen.

Aufmacherbild: Networking concept: magnifying optical glass with Cloud Network von Shutterstock / Urheberrecht: Maksim Kabakou

[ header = Seite 2: Erstellung des Backends ]

Erstellung des Backends

Nach der Anmeldung bei apiOmat gelangen wir in das Dashboard (Abb. 1). Dort wird zunächst eine App erstellt und das Push-Modul hinzugefügt. Die App trägt hier den Namen PushBaecker. Im Fenster Add Push Module muss der API-Server-Key des verwendeten GCM-Projekts eingetragen werden.

Abb. 1: Der App-Set-up-Screen im Dashboard von apiOmat

Nun wechselt man in den Class Editor. Danach wird über die Schaltfläche New Class die Klasse Picture angelegt. Diese erhält ein Attribut picture vom Typ Image.

Abb. 2: Der Class Editor

Durch einen Klick auf Deploy wird das Backend bereitgestellt. Um das Backend mit einem Android-Frontend zu nutzen, wird das Android-SDK benötigt. Dieses findet man im Tab SDK.

[ header = Seite 3: Entwicklung der ersten App ]

Die erste Android-App ist für den Bäcker. Mit ihr kann er die Push-Nachrichten verschicken. Dazu legen wir ein Android-Projekt mit mindestens Version 8 des SDK an. Anschließend binden wir das zuvor heruntergeladene SDK ein, indem wir die beiden entpackten Ordner in den src-Ordner kopieren. Im Layout der App legen wir eine ImageView, einen Button, ein EditTextfeld und einen weiteren Button an. Das Ganze wird in einer ScrollView eingebettet (Listing 1).

<ScrollView xmlns:android=http://schemas.android.com/apk/res/android
  android:layout_width="fill_parent"
  android:layout_height="fill_parent" >

    <LinearLayout
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:orientation="vertical" >

        <ImageView
          android:id="@+id/img_main_image"
          android:layout_width="200dip"
          android:layout_height="200dip"
          android:layout_gravity="center_horizontal"
          android:contentDescription="@string/imageDesc" />

        <Button
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:onClick="selectPicture"
          android:text="@string/imageUpload" />

        <EditText
          android:id="@+id/et_main_message"
          android:layout_width="fill_parent"
          android:layout_height="wrap_content"
          android:gravity="top"
          android:hint="@string/message"
          android:inputType="textMultiLine"
          android:lines="5"
          android:singleLine="false"/>

        <Button
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:onClick="savePicture"
          android:text="@string/sendPush" />
    </LinearLayout>

</ScrollView>

Sobald die App gestartet wird, soll der Bäcker gegen den Server authentifiziert werden. Dazu wird zunächst versucht, den User mit dem angegebenen Benutzernamen zu laden. Ist der User nicht bereits vorhanden, wird er neu angelegt (Listing 2).

/**
 * @author apiomat
 */
public class MainActivity extends Activity {

  final static User user = new User();
  private static final int SELECT_PHOTO = 100;
  /**
   * EditText for the PushMessage
   */
  public static EditText text;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    // Connect EditText with EditTextView
    text = (EditText) findViewById(R.id.et_main_message);
    user.setUserName("Paul");
    user.setPassword("Paul1");
    Datastore.configure(user);
    // try to load User
    user.loadMeAsync(this.userLoadAsync);

  }

  private final AOMEmptyCallback userLoadAsync = new AOMEmptyCallback() {

    @Override
    public void isDone(ApiomatRequestException exception) {

      if (exception != null) {
        // If the User doesn‘t exist create it
        user.saveAsync(new AOMEmptyCallback() {

          @Override
          public void isDone(ApiomatRequestException exception) {

            Log.i(MainActivity.class.getName(), "User saved");
          }
        });

      }
    }
  };
}

Die nächste Methode wird vom Button mit der Beschriftung Bild auswählen ausgelöst. Sie öffnet die Galerie, in der man dann das Bild für die Nachricht auswählen kann (Listing 3).

/**
/**
   * select a picture from android gallery
   * 
   * @param view
   */
public void selectPicture(View view) {
  // starts Intent to Pick an image
  Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
  photoPickerIntent.setType("image/*");
  startActivityForResult(photoPickerIntent, SELECT_PHOTO);
}

// will be called when intent finished and returns a result object
@Override
protected void onActivityResult(int requestCode, int resultCode,
      Intent imageReturnedIntent) {
  super.onActivityResult(requestCode, resultCode, imageReturnedIntent);

  switch (requestCode) {
    case SELECT_PHOTO:
    if (resultCode == RESULT_OK) {
      Uri selectedImage = imageReturnedIntent.getData();
      // gets Media Database
      String[] filePathColumn = { MediaColumns.DATA };
      // Cursor searches for selected image
      Cursor cursor = getContentResolver().query(selectedImage,
          filePathColumn, null, null, null);
      cursor.moveToFirst();

      int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
      String filePath = cursor.getString(columnIndex);
      cursor.close();

      // set Image to ImageView
      Bitmap img = BitmapFactory.decodeFile(filePath);
      ImageView image = (ImageView) findViewById(R.id.img_main_image);
      image.setImageBitmap(img);

    }
  }
}

Im nächsten Schritt wird zunächst ein Objekt der Klasse Picture auf dem Server erstellt. Danach wird das Bild als ByteArray angehängt und das Objekt wieder auf dem Server gespeichert. Das Bild erhält automatisch einen URL als Attribut, den man über die getImage-Methode holen kann. Mit den übergebenen Parametern ist es möglich, das Bild serverseitig skalieren zu lassen. Der URL wird dann in eine Map gespeichert und der Methode pushPhotoAndText übergeben (Listing 4).

/**
   * Get picture from imageView and save it
   * 
   * @param view
   */
public void savePicture(View view) {
  final ImageView image = (ImageView) findViewById(R.id.img_main_image);
  final Map<String, String> map = new HashMap<String, String>();
  Bitmap bitmap = ((BitmapDrawable) image.getDrawable()).getBitmap();
  ByteArrayOutputStream stream = new ByteArrayOutputStream();
  bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
  final byte[] byteArray = stream.toByteArray();
  final Pictures picture = new Pictures();
  picture.saveAsync(new AOMEmptyCallback() {

    @Override
    public void isDone(ApiomatRequestException exception) {
      Log.i("MainActivity", "SaveAsync is done");
      picture.postPictureAsync(byteArray, new AOMEmptyCallback() {

        @Override
        public void isDone(ApiomatRequestException exception) {

          Log.i("MainActivity", "PostAsync is done");
          String imgUrl = picture.getPictureURL(400, 400,
              "ffffff", null, "png");

          map.put("imgURL", imgUrl);

          pushPhotoAndText(map);

        }
      });
    }
  });
}

Die Methode pushPhotoAndText holt sich die Benutzernamen aller registrierten User vom Server, damit jeder die Nachricht empfängt. Hier wird außerdem die übergebene Map als CustomMap angehängt und der Text aus dem Textfeld als Nachricht festgelegt. Durch das Speichern auf dem Server wird die Nachricht abgeschickt.

public void pushPhotoAndText(final Map map) {
  User.getUsersAsync("", new AOMCallback<List<User>>() {

    @Override
    public void isDone(List<User> resultObject,
        ApiomatRequestException exception) {
      List<String> usernames = new ArrayList<String>();
      for (int i = 0; i < resultObject.size(); i++) {
        usernames.add(resultObject.get(i).getUserName());
      }

      PushMessage pushMessage = new PushMessage();
      EditText text = (EditText) findViewById(R.id.et_main_message);

      pushMessage.setCustomData(map);
      pushMessage.setPayload(text.getText().toString());
      pushMessage.setReceiverUserNames(usernames);
      pushMessage.saveAsync(new AOMEmptyCallback() {

        @Override
        public void isDone(ApiomatRequestException exception) {
          if (exception == null) {
            Log.i(MainActivity.class.getName(),
                "Message has been sent");
          } else {
            Log.e(MainActivity.class.getName(),
                "Message hasn't been sent");
            
          }

        }
      });
    }
  });

}

Die App für den Bäcker benötigt keine weiteren Berechtigungen – nur Zugriff auf das Internet. Damit die App diese Berechtigung einfordert, fügen wir im AndroidManifest folgende Zeile als Kind-Element von <manifest …> ein:

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

Damit ist die erste App fertiggestellt.

[ header = Seite 4: Entwicklung der zweiten App ]

Die zweite App erstellen wir mindestens mit Version 11 des SDK. Das Layout gestaltet sich mit zwei Textfeldern und einer ImageView übersichtlicher (Listing 6).

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/txt_main_open"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:singleLine="false" />

    <TextView
        android:id="@+id/tx_main_message"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="20sp" />

    <ImageView
        android:id="@+id/img_main_image"
        android:layout_width="400dip"
        android:layout_height="400dip"
        android:contentDescription="@string/imgDesc" />

</LinearLayout>

Die Aufgabe der MainActivity (Listing 7) beschränkt sich in der Client-App auf das Authentifizieren gegen den Server und die Anmeldung bei GCM. Für den praktischen Einsatz wäre es natürlich sinnvoll, wenn der Benutzer seinen Benutzernamen und das Passwort selbst festlegen könnte, besonders, falls noch andere Funktionen wie z. B. ein Chat eingefügt werden sollen. Da wir für die Push Notification GCM verwenden, müssen wir GCM erst in der App einrichten. Dazu befindet sich ein Hinweis im Kasten „GCM einbinden“.

GCM einbinden
– Zunächst benötigt man die Library „Google Cloud Messaging for Android“. Diese lädt man über den SDK-Manager herunter.
– Danach bindet man die Datei gcm.jar als Library in unsere App ein. Die Datei befindet sich im Ordner des Android-SDK unter …extrasgooglegcmgcm-clientdist.

Um die Datei als Library hinzuzufügen, muss diese nur in den libs-Ordner des Android-Projekts kopiert werden.

/**
 * @author apiomat
 */
public class MainActivity extends Activity {

  final static User user = new User();
  public static TextView pushtext;
  public static ImageView image;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    TextView open = (TextView) findViewById(R.id.txt_main_open);
    open.setText("An allen Tagen 24h geöffnet. Sonntag Ruhetag");
    pushtext = (TextView) findViewById(R.id.tx_main_message);
    image = (ImageView) findViewById(R.id.img_main_image);
    user.setUserName("USERNAME");
    user.setPassword("PASSWORD");
    Datastore.configure(user);

    user.loadMeAsync(this.userLoadAsync);
  }

  private final AOMEmptyCallback userLoadAsync = new AOMEmptyCallback() {
    @Override
    public void isDone(ApiomatRequestException exception) {
      if (exception != null) {

        user.saveAsync(MainActivity.this.userSaveAsync);

      } else {
        // calls the "onRegistered" method in GCMIntentService
        final String regId = GCMRegistrar
            .getRegistrationId(MainActivity.this);
        if (regId.equals("")) {
          // replace 373389763586 with the number of YOUR GCM project
          GCMRegistrar.register(MainActivity.this, "373389763586");
        } else {
          Log.i("push", "Already registered");
          MainActivity.user.setRegistrationId(regId);

          MainActivity.user.saveAsync(MainActivity.this.userSaveAsyncWithRegistrationId);
        }
      }
    }
  };

  private final AOMEmptyCallback userSaveAsync = new AOMEmptyCallback() {

    @Override
    public void isDone(ApiomatRequestException exception) {
      // calls the "onRegistered" method in GCMIntentService
      GCMRegistrar.register(MainActivity.this, "373389763586");
    }
  };

  private final AOMEmptyCallback userSaveAsyncWithRegistrationId = new AOMEmptyCallback() {

    @Override
    public void isDone(ApiomatRequestException exception) {

    }
  };
}

Etwas umfangreicher sieht es in der Klasse GCMIntentservice aus, die von GCMBaseIntentService erbt. Beim Anlegen der Klasse lassen wir alle abstrakten Methoden mit erstellen. Durch das Überschreiben der onMessage-Methode wird bei eingehender Nachricht der Text der Nachricht in ein Textfeld geschrieben und der URL des Bilds aus der CustomMap gelesen (Listing 8).

/**
 * GCMIntentService More Information:
 * http://developer.android.com/google/gcm/gs.html
 * 
 * @author apiomat
 */
public class GCMIntentService extends GCMBaseIntentService {
  @Override
  protected void onError(Context arg0, String arg1) {

  }

  @Override
  protected void onMessage(Context arg0, Intent msg) {

    final String imgURL = msg.getExtras().getString("imgURL");
    final String gcmMessage = msg.getExtras().getString("payload");
    Handler h = new Handler(Looper.getMainLooper());
    h.post(new Runnable() {
      @Override
      public void run() {
        try {
          MainActivity.pushtext.setText(gcmMessage);

          loadPicture(imgURL);

          notification(gcmMessage);
        } catch (Exception gcmexception) {
          Log.i("GCMIntentService", "App nicht aktiv");
        }

      }
    });
  }

  @Override
  protected void onRegistered(Context arg0, String regId) {
    MainActivity.user.setRegistrationId(regId);
    MainActivity.user.saveAsync(new AOMEmptyCallback() {
      @Override
      public void isDone(ApiomatRequestException exception) {
        Log.i("GCMIntentService", "User has been saved");
      }
    });

  }

[...]Ausgelassene abstrakte Methoden

Die dabei aufgerufene Methode notification erstellt eine Android Notification mit dem Text der Push-Nachricht, einem Alarmton, und lässt, falls vorhanden, die Nachrichten-LED des Geräts blau blinken (Listing 9).

void notification(String gcmMessage) {
  NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

  Intent intent = new Intent(this, MainActivity.class);
  PendingIntent pIntent = PendingIntent.getActivity(this, 0, intent, 0);
  Uri alarmSound = RingtoneManager
      .getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);

  Notification noti = new Notification.Builder(this)
      .setContentTitle("Neuigkeiten vom Bäcker")
      .setContentText(gcmMessage).setLights(888, 1000, 1000)
      .setSmallIcon(R.drawable.ic_launcher)
      .setWhen(System.currentTimeMillis()).setSound(alarmSound)
      .setContentIntent(pIntent).build();

  noti.flags |= Notification.FLAG_AUTO_CANCEL;
  notificationManager.notify(0, noti);
}

Die Methode loadPicture erstellt ein Objekt der Klasse LoadImageTask. Die Klasse selbst erstellen wir im nächsten Schritt.

void loadPicture(String imgURL) {
  LoadImageTask loadImageTask = new LoadImageTask(MainActivity.image);
  loadImageTask.execute(imgURL);
}

Da man Netzwerkoperationen in Android nicht synchron ausführen kann, müssen wir das Bild über einen asynchronen Task vom Server laden. Die zu erstellende Klasse leiten wir dazu von der Klasse AsyncTask ab (Listing 10).

/**
 * load images async from web
 * 
 * @author apiomat
 */
public class LoadImageTask extends AsyncTask<String, Void, Bitmap>
{

  ImageView view = null;

  LoadImageTask( ImageView view )
  {
    this.view = view;
  }

  @Override
  protected Bitmap doInBackground( String... images )
  {
    try
    {
      URL url = new URL( images[ 0 ] );
      Log.i( "LoadImageTask", url.toString( ) );
      return BitmapFactory.decodeStream( url.openConnection( )
        .getInputStream( ) );
    }
    catch ( Exception e )
    {
      Log.e( "LoadImageTask", "Error occured. " + e.getMessage( ) );
    }
    return null;
  }

  @Override
  protected void onPostExecute( Bitmap bitmap )
  {
    this.view.setImageBitmap( bitmap );
  }
}

Die Anpassungen des Android-Manifests gestalten sich in der App für die Kunden umfangreicher. GCM erfordert das Hinzufügen von weiteren Permissions, Intents und BroadcastReceivern. Der launchMode der Activity wird außerdem auf singleTask gesetzt (Listing 11), damit beim Anklicken der Notification die richtige Instanz der Activity (also jene, die die Nachricht und das Bild enthält) gestartet wird.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="de.thronschecter.pushladenkunde"
  android:versionCode="1"
  android:versionName="1.0" >

  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
  <!-- GCM connects to Google Services. -->
  <uses-permission android:name="android.permission.INTERNET" />
  <!-- GCM requires a Google account. -->
  <uses-permission android:name="android.permission.GET_ACCOUNTS" />
  <!-- Keeps the processor from sleeping when a message is received. -->
  <uses-permission android:name="android.permission.WAKE_LOCK" />

  <permission
      android:name="de.thronschecter.pushladenkunde.permission.C2D_MESSAGE"
      android:protectionLevel="signature" />

  <uses-permission android:name="de.thronschecter.pushladenkunde.permission.C2D_MESSAGE" />
  <uses-sdk
      android:minSdkVersion="11"
      android:targetSdkVersion="17" />

  <application
      android:allowBackup="true"
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name"
      android:theme="@style/AppTheme" >
    <service android:name=".GCMIntentService" />
    <activity
          android:name="de.thronschecter.pushladenkunde.MainActivity"
          android:label="@string/app_name" 
          android:launchMode="singleTask">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <receiver
          android:name="com.google.android.gcm.GCMBroadcastReceiver"
          android:permission="com.google.android.c2dm.permission.SEND" >
      <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE" />
        <action android:name="com.google.android.c2dm.intent.REGISTRATION" />

        <category android:name="my_app_package" />
      </intent-filter>
    </receiver>
  </application>

</manifest>

Damit sind die beiden Apps einsatzbereit.

[ header = Seite 5: Ergebnis ]

An diesem Beispiel wurde gezeigt, dass man mit wenig Aufwand eine App für ein realitätsnahes Szenario erstellen kann. Würde man diese Funktion auf eigenen Servern implementieren, müsste man auf diesen eine Datenbank, die GCM-Server-App, Firewalls, Schnittstellen, Authentifizierung, Bilderupload usw. implementieren. Weiterhin wäre das Ansprechen des Backends in der Client-App sehr viel umfangreicher.

Abb. 3: Die App für den Bäcker

Abb. 4: Die App für die Kunden

Trotz der hier verdeutlichten immensen Abstraktion und Vereinfachung ist Backend as a Service in Deutschland noch recht unbekannt. Dabei gestaltet sich der gesamte Entwicklungsprozess einer App nicht nur schneller und einfacher, sondern auch wesentlich kostensparender. Die hier gezeigten Apps könnten zum Beispiel von einem Entwickler an einem Tag fertiggestellt werden. Auch falls Anpassungen an der App oder die gestiegene Zahl der User einen bezahlten Plan erfordern, liegen die Kosten immer unter dem des eigenen Hostings.

Datenschutz ist nicht erst seit den Enthüllungen von Edward Snowden ein großes Thema. So hegen auch viele deutsche Entwickler Bedenken, ihre Daten einem Drittanbieter anzuvertrauen. Einerseits bietet jedoch jeder große Backend-as-a-Service-Anbieter die Möglichkeit, die Daten zu exportieren; andererseits sollte jeder, der auf die Sicherheit seiner Daten achtet, einen deutschen Anbieter verwenden.

Geschrieben von
Peter Hoffmann
Peter Hoffmann
Peter Hoffmann studiert Technische Redaktion und E-Learning- Systeme an der Hochschule Merseburg. Zurzeit unterstützt er neben dem Studium die Apinauten GmbH in Leipzig. In seiner Freizeit beschäftigt er sich mit Android-Programmierung, Mobile-Apps und Fotografie.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.