Smarthome App #10: Modularisierung der App


21.04.2017  |  Tutorial, Smarthome App

Im heutigen Tutorial wird die Struktur des Projektes angepasst und modularisiert, damit die Programmierung im weiteren Verlauf der Tutorialreihe effizienter und mit weniger Arbeit verläuft. Dazu werden einige Methoden und innere Klassen in eigene neue Klassen ausgelagert. Der Code kann dann einfacher verwendet und erweitert werden.

Einteilung in Packages

Als erstes erstellst du im Java-Verzeichnis des Projektes ein neues Package, indem du mit rechts auf den Ordner "de.smarthome_blogger.smarthome" klickst und "New -> Package" wählst.
Mit einem Rechtsklick kannst du ein neues Paket erstellen.
Gib dem Package den Namen "system" und klicke auf "OK".
Nenne das neue Paket
Verschiebe nun die Klassen "HTTPRequest", "SaveData" und "Icons" in das neue Paket. In dem Fenster, das sich nun öffnet, klickst du auf "Refactor".
Mit der Refaktorisierung wird das Projekt automatisch an die neue Struktur angepasst.

Eigene Klasse für Dialoge und Fehlermeldungen

Jetzt erstellst du im Package "system" die Klasse "Dialogs". Dazu klickst du das Package mit rechts an und wählst "New -> Java Class". Trage den Namen ein und klicke auf "OK". In der Klasse "Dialogs", wird die Methode fehlermeldung() erstellt, die als Parameter einen String und eine View erhält. Die Methode erstellt eine Snackbar und zeigt die übergebene Nachricht in der übergebenen View an:
/**
* Stellt die angegebene Nachricht in einer Snackbar dar
* @param msg die Nachricht
* @param v die View, in der die Nachricht angezeigt werden soll
*/
public static void fehlermeldung(String msg, View v){
	Snackbar.make(v, msg, Snackbar.LENGTH_SHORT).show();
}
Alle Klassen, die bisher eine eigene Methode fehlermeldung() hatten, sollen nun die neue Methode aufrufen. Dazu muss jede betroffene Klasse angepasst werden. Öffne als erstes die Klasse SettingsFragment und lösche die Methode fehlermeldung() daraus. Ändere nun jeden verbleibenden Aufruf von fehlermeldung(), indem du als Parameter "settingsView.findViewById(R.id.frame)" hinzufügst, und die Klasse Dialogs entweder mit Alt & Eingabe oder mit dem Import-Befehl "import static de.smarthome_blogger.smarthome.system.Dialogs.fehlermeldung;" am obersten Teil der Java-Datei importierst. Als nächstes ist die Klasse RoomFragment dran. Auch hier wird die Methde fehlermeldung() gelöscht und die verbleibenden Aufrufe angepasst. Als Parameter wird hier "roomView.findViewById(R.id.frame)" hinzugefügt. Anschließend importierst du auch hier die Methode fehlermeldung() entweder mit Alt & Eingabe oder mit dem Import-Befehl. Die Klasse LoginActivity ist die letzte Klasse, in der die Methode fehlermeldung() gelöscht wird. Hier werden ebenfalls die Aufrufe angepasst, indem sie um "findViewById(R.id.frame)" erweitert werden. Auch die Methode wird wieder entsprechend importiert. In der Methode login() wird in der Interface-Methode onRequestResult() für den Fall, dass das Ergebnis "unknownuser" ist, keine Fehlermeldung ausgegeben, sondern die Methode setError() des Textfeldes "usernameField" aufgerufen. Damit wird der Fehler direkt beim entsprechenden Textfeld angezeigt.
So sieht eine Fehlermeldung direkt im Textfeld aus.
Der angepasste Code für den Aufruf lautet dann:
usernameField.setError(Dieser Nutzer existiert nicht);
Auch in der Methode onCreate(), wo dem Login-Button der OnClickListener zugewiesen wird, werden die Aufrufe der Methode fehlermeldung() mit Aufrufen der setError()-Methode ersetzt. Dazu wird immer die setError()-Methode des betroffenen Textfeldes aufgerufen. Der if-Block sollte nun in etwa folgendermaßen aussehen:
//Eingaben auf Vollständigkeit prüfen
if(username.equals("")){
	usernameField.setError("Bitte gib deinen Nutzernamen ein");
}
else if(password.equals("")){
	passwordField.setError("Bitte gib dein Passwort ein");
}
else if(serverIp.equals("")){
	serverIpField.setError("Bitte gib die Adresse des Servers ein");
}
else{
	login(username, password, serverIp, saveLogin.isChecked());
}

Items und Adapter auslagern

Nun wird ein weiteres Package erstellt. Klicke dazu wieder mit rechts auf das Hauptpaket und wähle "New -> Package". Gib dem Paket den Namen "adapters" und klicke auf "OK". In diesem Paket wird nun eine neue Klasse namens "RoomAdapter" erstellt. Öffne die Klasse "RoomFragment" und schneide die Klasse "RoomAdapter" daraus aus. Mit dem ausgeschnittenem Code ersetzt du nun in der neu angelegten Klasse diesen Codeblock:
public class RoomAdapter{

}
Innerhalb der Klasse wird nun ein Konstruktor für die Klasse erstellt, damit sie verwendet werden kann. Füge dazu unter der Variable "int lastPosition = -1;" den folgenden Code ein:
boolean setState = false;

ArrayList<RoomItem> roomItems;
Activity activity;
Context context;
View view;

public RoomAdapter(ArrayList<RoomItem> roomItems, Activity activity,
Context context, View view){
	this.roomItems = roomItems;
	this.activity = activity;
	this.context = context;
	this.view = view;
}
Da die Klasse "RoomItem" noch nicht existiert, wird sie nun erstellt. Zuvor wird jedoch ein Package für Items im Hauptpaket angelegt. Wähle dazu wieder "New -> Package", gib ihm den Namen "items" und klicke auf "OK". In diesem Package erstellst du jetzt eine neue Klasse namens "RoomItem". Schneide aus der Klasse "RoomFragment" die Klasse "RoomItem" aus und ersetze damit in der neu angelegten Klasse den folgenden Code:
public class RoomItem{

}
Füge der Klasse ein neues String-Attribut namens "room" hinzu, in dem der Raum des Elements abgespeichert wird. Erweitere danach den Konstruktor um einen String-Parameter namens "room" und ergänze den Rumpf des Konstruktors um:
this.room = room;
Außerdem wird eine neue Getter-Methode für den Raum erstellt:
/**
 * Gibt den Raum des Ger&auml;tes zur&uuml;ck
 * @return
 */
public String getRoom(){
	return room;
}
Auch wird das Attribut "device" in "id" umbenannt. Ändere diesen Namen bei den Attributen, beim Parameter des Konstruktors, bei der Wertezuweisung im Konstruktor und benenne die Methode getDevice() in getId() um. Diese Methode gibt statt "device" nun "id" zurück. Wechsle wieder zur Klasse "RoomFragment", springe zu einer Stelle, an der ein "RoomItem" verwendet wird und füge ganz oben, über der Klassendefinition die folgenden Import-Befehle hinzu:
import de.smarthome_blogger.smarthome.items.RoomItem;
import de.smarthome_blogger.smarthome.adapters.RoomAdapter;
In der Methode loadRoomData() wird in der Interface-Methode onRequestResult() der Konstruktor-Aufruf des RoomAdapters (bei mir in Zeile 126) an die Parameter angepasst:
roomAdapter = new RoomAdapter(roomItems, getActivity(), getContext(),
roomView.findViewById(R.id.frame));
Wechsle in die Klasse "RoomAdapter" und ersetze ganz oben den Import-Befehl:
import de.smarthome_blogger.smarthome.RoomFragment;
mit dem folgendem:
import de.smarthome_blogger.smarthome.items.RoomItem;
Falls dir in der Klasse "RoomAdapter" weitere Fehler angezeigt werden, entferne bei allen Aufrufen von "RoomFragment.RoomItem" das "RoomFragment.". Erstelle nun im Package "items" eine neue Klasse namens "SettingItem". Wechsle zur Klasse "SettingsFragment" und schneide die Klasse "SettingItem" daraus aus. Damit ersetzt du nun in der neu erstellten Klasse diesen Codeblock:
public class SettingItem{

}
Da die eingefügte Klasse nicht public ist, fügst du vor das "class SettingItem" das Wort "public" ein. Wechsle nun wieder zur Klasse "SettingsFragment" und schneide dort auch die Klasse "SettingsAdapter" aus. Erstelle im Package "adapters" eine neue Klasse namens "SettingAdapter" und ersetze dort den folgenden Codeblock mit dem ausgeschnittenen Code:
public class SettingAdapter{

}
Benenne die Klasse nun von "SettingsAdapter" um in "SettingAdapter". Füge dann am oberen Ende der Klasse "SettingsFragment" diese beiden Import-Befehle ein:
import de.smarthome_blogger.smarthome.adapters.SettingAdapter;
import de.smarthome_blogger.smarthome.items.SettingItem;
Der SettingAdapter greift in der Methode setAnimation() (Zeile 55) auf die Methode getContext() zu. Da sich die Klasse nicht innerhalb eines Fragments befindet, ist dieser Zugriff aber nicht erlaubt. Ersetze daher den Aufruf von getContext() mit "context". Auch die Verwendung von "settingItems" funktioniert nicht, da dieses Attribut noch nicht existiert. Deswegen fügst du über der Methode getItemCount() diese beiden Attribute und den Konstruktor hinzu:
ArrayList<SettingItem> settingItems;
    Context context;

    public SettingAdapter(ArrayList<SettingItem> settingItems, Context context){
        this.settingItems = settingItems;
        this.context = context;
    }
Anschließend wechselst du wieder zum SettingsFragment und passt in Zeile 142 den Konstruktoraufruf von "SettingAdapter" an die neuen Parameter an:
//So sollte der Aufruf nun aussehen
settingsAdapter = new SettingAdapter(settingItems, getContext());
In der Klasse RoomFragment kann das Attribut "setState" entfernt werde, da es in den RoomAdapter ausgelaert wurde. Entferne dazu über dem Konstruktor "RoomFragment" den Code:
boolean setState = false;
Am unteren Ende der Klasse kannst du außerdem die Methoden setModes(), showOverview(), openHeatingDialog() und openSceneMenu() löschen, da diese gleich in die Klasse RoomControl ausgelagert werden. Entferne dazu den folgenden Code:
/**
 * Schaltet das Ger&auml;t device auf den Zustand mode
 * @param item
 * @param mode
 */
public void setModes(final RoomItem item, boolean mode){}

/**
 * Zeigt den Graphen f&uuml;r den Sensor sensor an
 * @param type
 * @param sensor
 */
public void showOverview(String type, String sensor){}

/**
 * Heizungs-Men&uuml; &ouml;ffnen
 */
public void openHeatingDialog(){}

/**
 * Szenen-Men&uuml; &ouml;ffnen
 */
public void openSceneMenu(){}

Steuerungsmethoden in eigene Klasse auslagern

Die Methoden, mit denen Geräte gesteuert werden, werden in eine neue Klassen namens "RoomControl" ausgelagert. Erstelle dazu im Paket "system" die Klasse "RoomControl" und öffne sie. Erstelle darin die Methode setModes():
/**
 * Schaltet das angegebene Ger&auml;t auf den Zustand mode
 * @param v die View
 * @param activity die aufrufende Activity
 * @param item das Item
 * @param indexOfItem der Index des Items
 * @param mode der Zustand, auf den das item geschaltet werden soll
 * @param notifyListener der NotifyListener
 */
public static void setModes(final View v, Activity activity, final RoomItem item,
final int indexOfItem,final boolean mode, final OnAdapterNotifyListener notifyListener){}
Die Methode erhält als Parameter eine View, eine Activity, ein RoomItem, den Index des Items in der aufrufenden RecyclerView, einen Zustand, auf den das Item geschaltet werden soll, und ein OnAdapterNotifyCallback. Mit dem Callback kann das Objekt, das die Funktion aufruft, über den Ausgang der HTTP-Anfrage benachrichtigt werden. Das Interface "OnAdapterNotifyCallback" definierst du unterhalb der Methode setModes() folgendermaßen:
public interface OnAdapterNotifyListener{
	void onNotify(int i);
}
Neben der Methode setModes() hat die Klasse "RoomControl" noch weitere Methoden, die in einem anderen Tutorial genauer implementiert werden. Vorerst werden nur die Methodenköpfe hinzugefügt:
/**
 * &Ouml;ffnet die Heizungssteuerung
 */
public static void openHeatingDialog(){}

/**
 * &Ouml;ffnet das Szenenmen&uuml;
 */
public static void openSceneMenu(){}

/**
 * Zeigt den Graphen f&uuml;r den &uuml;bergebenen Sensor an
 * @param activity aufrufende Activity
 * @param item das Item
 * @param location der Ort des Items
 */
public static void showOverview(Activity activity, RoomItem item, String location){}
Jetzt wechselst du wieder zur Klasse "RoomAdapter" und bearbeitest den Aufruf der Methode setModes() (bei mir in Zeile 80):
RoomControl.setModes(view, activity, ri, i, !switchViewHolder.switchView.isChecked(),
	new RoomControl.OnAdapterNotifyListener() {
		@Override
		public void onNotify(int i) {
			notifyItemChanged(i);
		}
	});
Nun wird das Element der RecyclerView an der Stelle i aktualisiert, wenn die Methode onNotify() des OnAdapterNotifyCallbacks aktualisiert wird. Auch der Aufruf von showOverview() wird angepasst. Aus "showOverview(ri.getDeviceType(), ri.getDevice())" wird dann:
RoomControl.showOverview(activity, ri, ri.getRoom());
Füge nun am oberen Ende der Klasse den Import-Befehl für die Klasse "RoomControl" hinzu:
import de.smarthome_blogger.smarthome.system.RoomControl;
In der Methode getItemViewType() der Klasse "RoomAdapter" wird jeder Aufruf von "roomItems.get(i).type" mit "roomItems.get(i).getType()" ersetzt, da diese Klassenvariablen private sind und man nur mit den Getter-Methoden darauf zugreifen kann. Außerdem wird in der Methode setAnimation() der Parameter getContext() mit "context" ersetzt, da der Aufruf außerhalb eines Fragments stattfindet und die Klasse ein Context-Attribut besitzt. Ändere auch jeden Aufruf von "ri.getDevice()" in "ri.getIcon()" (bei mir in den Zeilen 60, 98 und 112). Danach ersetzt du in Zeile 132 den Aufruf von "openSceneMenu()" mit "RoomControl.openSceneMenu()" und in Zeile 119 den Aufruf von "openHeatingDialog() mit "RoomControl.openHeatingDialog()". Wechsle zur Klasse "RoomFragment" und ergänze den Konstruktor-Aufruf der Klasse RoomItem um "location" (Zeile 124). Bei Fragen oder Problemen kannst du mir gerne einen Kommentar hinterlassen.

Über den Autor


Sascha Huber

Hallo, ich bin Sascha, der Gründer von Smarthome Blogger.

Mit einer Leidenschaft für Technologie und einem Hintergrund als Software Engineer habe ich 2016 Smarthome Blogger gegründet. Mein Ziel war es schon immer, innovative Lösungen zu entdecken, die unser Leben einfacher und intelligenter gestalten können. In meinem beruflichen Leben arbeite ich täglich mit Software und Technik, aber auch in meiner Freizeit bin ich stets auf der Suche nach neuen technischen Spielereien und Möglichkeiten, mein Zuhause zu automatisieren und zu verbessern.

Auf Smarthome Blogger teile ich mein Wissen, meine Erfahrungen und meine Begeisterung für alles rund um das Thema Smarthome.



Dieser Beitrag hat dir gefallen?

Dann abonniere doch unseren Newsletter!