In Teil 2 haben wir den RecyclerView erstellt und, natürlich, die Haupt-Activity. Das ist schonmal nicht schlecht, aber immernoch nicht sehr nützlich, weil wir die App nicht wirklich benutzen können (NOCH nicht!). Dieses Mal werden wir nocheine Activity erstellen, die die Details von einem Lease anzeigt und uns neue anlegen und existierende löschen lässt – it is basically close to CRUD.
Eigentlich sollte diese Serie 3 Teile haben, aber während dieses Teils haben wir festgestellt, dass der 3. Teil einfach zu lang wird. Wir haben uns daher entschlossen, die Serie vierteilig zu machen…
- Verschieben der Session-Methoden in das Application-Objekt
- Neue Activity
- Hinzufügen des onClick-Events im RecyclerView
- Laden von Detail-Daten
Verschieben der Session-Methoden in das Application-Objekt
IM 2. Teil haben wir an ein paar Stellen schmutzige Tricks verwendet, damit wir schnell vorzeigbare Ergebnisse bekommen. Diese würden in der Zukunft aber zu mehr Arbeit führen. Einer dieser Tricks war, die Datenbanksession in der Activity zu erstellen – diese werden wir nun in die von Application abgeleitete Klasse unserer App verschieben. Vielleicht fragt ihr jetzt „Application? Hatten wir das irgendwo?“ – nein, hatten wir nicht. Daher werden wir im ersten Schritt eine Klasse erstellen, die wir von android.app.Application ableiten. Bitte rechtsklickt das Package „com.devteam83.tutorials.leasegreendao“ (oder wie euer Package heisst), wählt „new“ -> „Java class“ und lasst sie die o.a. Klasse erweitern:
package com.devteam83.tutorials.leasegreendao; import android.app.Application; public class LeaseApplication extends Application { }
Um es dieser Klasse zu ermöglichen, eine Referenz auf unsere Session-Klasse zu halten, braucht Sie ein Member-Attribut, das natürlich irgendwo initialisiert werden muss. Dabei hilft uns Android Studio – einfach irgendwo mit der rechten Maustaste klicken und „Generate…“ -> „Override Methods…“, danach dann die „onCreate“-Methode von der Klasse Application auswählen:
Schonmal nicht schlecht. Wir müssen nur noch den Code aus MainActivity einfügen und einen Getter für daoSession erstellen (Rechtsklick -> „generate…“ usw., ihr kennt das ja schon 😉 ). Danach sieht die Klasse so aus:
package com.devteam83.tutorials.leasegreendao; import android.app.Application; import android.database.sqlite.SQLiteDatabase; import com.devteam83.tutorials.leasegreendao.model.DaoMaster; import com.devteam83.tutorials.leasegreendao.model.DaoSession; public class LeaseApplication extends Application { public DaoSession daoSession; @Override public void onCreate() { super.onCreate(); DaoMaster.DevOpenHelper helper = new DaoMaster.DevOpenHelper(this, "lease-db", null); SQLiteDatabase db = helper.getWritableDatabase(); DaoMaster daoMaster = new DaoMaster(db); daoSession = daoMaster.newSession(); } public DaoSession getDaoSession() { return daoSession; } }
Bitte achtet auf folgende Dinge:
Bitte entfernt nicht den automatisch generierten Aufruf von super.onCreate();
Achtet auf die letzte Zeile „daoSession = daoMaster.newSession();“, im Gegensatz zum Code in MainActivity wurde der Klassenname DaoSession vor dem Attributnamen entfernt. Würden wir das nicht machen, dann würden wir eine lokale Variable mit limitierter Sichtbarkeit erstellen. Das wäre hier nicht sehr sinnvoll, daher benutzen wir das Attribut der Klasse.
Aus MainActivity können wir den kompletten Block entfernen und eine Zeile einfügen (durchgestrichen = entfernen):
DaoMaster.DevOpenHelper helper = new DaoMaster.DevOpenHelper(this, "lease-db", null); SQLiteDatabase db = helper.getWritableDatabase(); DaoMaster daoMaster = new DaoMaster(db); DaoSession daoSession = daoMaster.newSession(); DaoSession daoSession = ((LeaseApplication) getApplicationContext()).getDaoSession();
Viel besser, oder? Dann probieren wir doch gleich mal aus, ob unsere App noch funktioniert. Klickt auf den „Play“-Knopf in der Werkzeugleiste (prüft vorher, ob links von dem Knopf auch die run/debug-Konfiguration „app“ ausgewählt ist).
WTF? (Übrigens, das bedeutet „What a Terrible Failure“ – wenn ihr mir nicht glaubt, dann seht selbst 😉
Wir bekommen:
Caused by: java.lang.ClassCastException: android.app.Application cannot be cast to com.devteam83.tutorials.leasegreendao.LeaseApplication at com.devteam83.tutorials.leasegreendao.MainActivity.onCreate(MainActivity.java:26) at android.app.Activity.performCreate(Activity.java:5937) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1105) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2251) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2360) at android.app.ActivityThread.access$800(ActivityThread.java:144) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1278) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:135) at android.app.ActivityThread.main(ActivityThread.java:5221) at java.lang.reflect.Method.invoke(Native Method) at java.lang.reflect.Method.invoke(Method.java:372) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
Exceptions? NEIN!!! Doch – aus einem einfachen Grund: Wir haben unsere Application-Klasse geändert und die App weiss davon noch nichts. wir müssen android:name=“.LeaseApplication“ zum AndroidManifest.xml hinzufügen. Meine sieht jetzt so aus:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.devteam83.tutorials.leasegreendao" > <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" android:name=".LeaseApplication"> <activity android:name=".MainActivity" android:label="@string/title_activity_main" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Danach sollte auch die App wieder funktionieren.
Neue Activity
Wir möchten die Daten unserer Leases in einer Activity anzeigen, die wiederum aus unserem RecyclerView gestartet wird – dementsprechend ist wohl der nächste logische Schritt eben diese Activity zu erstellen. Also macht bitte wieder einen Rechtsklick auf das Package „com.devteam83.tutorials.leasegreendao“ und wählt „New“ -> „Activity“ -> „Blank Activity“ und nehmt folgende Einstellungen in dem Wizard aus:
Bitte wechselt nach dem Wizard auf „Text“ (Design? Nee!) und entfernt die TextView aus dem generierten Layout. Fügt bitte stattdessen ein LinearLayout (mit android:orientation=“vertical“) und, in dem LinearLayout, zwei EditTexts, einem Spinner (sorry, heisst so), und zwei Buttons ein. Danach sieht die Datei so aus:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context="com.devteam83.tutorials.leasegreendao.LeaseActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <EditText android:id="@+id/activity_lease_item" android:layout_width="match_parent" android:layout_height="wrap_content" /> <EditText android:id="@+id/activity_lease_comment" android:layout_width="match_parent" android:layout_height="wrap_content" /> <Spinner android:id="@+id/activity_lease_person" android:layout_width="match_parent" android:layout_height="wrap_content"></Spinner> <Button android:id="@+id/activity_lease_save" android:text="save" android:layout_width="match_parent" android:layout_height="wrap_content" /> <Button android:id="@+id/activity_lease_delete" android:text="delete" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout> </RelativeLayout>
Dies waren eigentlich nur Wiederholungen von Dingen, die wir vorher schon gemacht haben – die Felder dienen dazu, unsere Daten später anzuzeigen. Bitte achtet auf die IDs. Wenn wir auf „Design“ wechseln, dann können wir die Felder bereits bewundern.
Jetzt brauchen wir Referenzen auf unsere Felder in LeaseActivity – wir werden uns nicht lange damit aufhalten (wenn es zu schnell geht lasst bitte einen Kommentar hier – dann erklären wir dies etwas detaillierter) und das Ergebnis sieht so aus (LeaseActivity.java):
public class LeaseActivity extends Activity { public EditText editTextItem; public EditText editTextComment; public Spinner spinnerPerson; public Button buttonSave; public Button buttonDelete; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_lease); editTextItem = (EditText) findViewById(R.id.activity_lease_item); editTextComment = (EditText) findViewById(R.id.activity_lease_comment); spinnerPerson = (Spinner) findViewById(R.id.activity_lease_person); buttonSave = (Button) findViewById(R.id.activity_lease_save); buttonDelete = (Button) findViewById(R.id.activity_lease_delete); } ...
Hinzufügen des onClick-Events im RecyclerView
Im Moment haben wir zwei separate Activities, die nichts voneinander wissen und wir haben noch keine Chance unsere LeaseActivity im Emulator oder auf einem Gerät anzusehen. Wenn ihr schon mit ListViews gearbeitet habt, dann gibt es ein paar Änderungen. Der ClickListener führt sich jetzt im ViewHolder zu Hause…
Das bedeutet, dass wir nun in MainActivityListAdapter.java weiterarbeiten – öffnet die Datei und scrollt bis ganz nach unten. Wir lassen unseren ViewHolder nun OnClickListener implementieren, indem wir die erste Zeile so abändern:
public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
Android Studio wird sich sofort beschweren, weil eine zu implementierende Methode fehlt, also machen wir das… richtig: -> „Generate…“ -> „Override Methods…“. Wählt die Methode „onClick(View v)“ aus der Klasse OnClickListener aus.
In dieser Methode können wir nun bestimmen, was beim Klicken passieren soll, aber die Art und Weise wie alles zusammenarbeitet macht es uns hier nicht leicht, Referenzen auf alle Dinge zu bekommen, die wir brauchen. Die beste Methode wäre, ein Interface zu erstellen, dass dann die Klick Events behandelt. MainActivity würde dann dieses Interface implementieren und wir würden MainActivity als Listener registrieren.
Da die Erzeugung von Schnittstellen nicht der zentrale Aspekt in diesem Tutorial ist, werden wir das nicht tun, und stattdessen unsere Activities und Handler usw. einfach direkt miteinander „verheiraten“. Dies ist sicherlich kein guter Stil…
Ich habe jetzt, über ViewHolder und MainActivityListAdapter, Referenzen auf MainActivity durchgeschleift, habe eine ID in das Eltern-Element im RecyclerView (in activity_main_item.xml) eingefügt und die Ereignismethode in ViewHolder(onClick) implementiert. Unsere Dateien sehen nun wie folgt aus:
MainActivityListAdapter.java: package com.devteam83.tutorials.leasegreendao; import android.content.Intent; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.devteam83.tutorials.leasegreendao.model.Lease; import java.util.List; public class MainActivityListAdapter extends RecyclerView.Adapter<MainActivityListAdapter.ViewHolder>{ private List<Lease> mData; private MainActivity mainActivity; public MainActivityListAdapter(List<Lease> data, MainActivity mainActivity) { this.mData = data; this.mainActivity = mainActivity; } @Override public MainActivityListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { // create a new view View itemLayoutView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.activity_main_item, null); // create ViewHolder ViewHolder viewHolder = new MainActivityListAdapter.ViewHolder(itemLayoutView, mainActivity); return viewHolder; } @Override public void onBindViewHolder(ViewHolder viewHolder, int position) { viewHolder.textViewItem.setText(mData .get(position) .getItem()); viewHolder.textViewPersonName.setText(mData .get(position) .getPerson() .getName()); viewHolder.leaseId = mData.get(position).getId(); } @Override public int getItemCount() { return mData.size(); } public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { public long leaseId; public MainActivity mainActivity; public TextView textViewItem; public TextView textViewPersonName; public View viewParent; @Override public void onClick(View v) { Intent intent = new Intent(mainActivity, LeaseActivity.class); intent.putExtra("lease_id", leaseId); mainActivity.startActivityForResult(intent, 1); } public ViewHolder(View itemLayoutView, MainActivity mainActivity) { super(itemLayoutView); this.mainActivity = mainActivity; textViewItem = (TextView) itemLayoutView.findViewById(R.id.activity_main_item_item); textViewPersonName = (TextView) itemLayoutView.findViewById(R.id.activity_main_item_person_name); viewParent = (View) itemLayoutView.findViewById(R.id.activity_main_item_parent); viewParent.setOnClickListener(this); } public void setLeaseId(long id) { this.leaseId = id; } } }
activity_main_item.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/activity_main_item_parent" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- item --> <TextView android:id="@+id/activity_main_item_item" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="16dp" /> <!-- person name --> <TextView android:id="@+id/activity_main_item_person_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="16dp" /> </LinearLayout>
in MainActivity.java [part]:
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); DaoSession daoSession = ((LeaseApplication) getApplicationContext()).getDaoSession(); LeaseDao leaseDao = daoSession.getLeaseDao(); List leaseList = leaseDao.loadAll(); RecyclerView recyclerView = (RecyclerView) findViewById(R.id.main_activity_recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); MainActivityListAdapter adapter = new MainActivityListAdapter(leaseList, this); recyclerView.setAdapter(adapter); }
Ich möchte gern noch wiederholen, dass ich diese ganzen Schritte sehr schnell abgehandelt habe – wenn es Probleme gibt, dann hinterlasst bitte einen Kommentar…
Wenn ihr die App jetzt startet, dann solltet ihr in der Lage sein, LeaseActivity durch Klicken auf ein Element im RecyclerView, aber alle Felder sind noch leer.
Laden von Detail-Daten
Der letzte Abschnitt war etwas viel, ich weiss, aber wir haben nicht mehr so viel zu tun und wir entfernen uns von dem einfachen Android Vor und Zurück. Wir haben bereits die IDs unserer Leases in den ViewHoldern – dort können wir sie direkt in den Intent zum Öffnen von LeaseActivity einfügen. Wir haben also alles was wir brauchen, um unsere Daten zu laden. Wir öffnen LeaseActivity.java und erstellen ein Attribut, das unseren Lease enthalten wird:
public Lease lease;
Am Ende von onCreate versuchen wir unseren Intent zu bekommen und aus diesem die Lease-ID auszulesen:
Intent intent = getIntent(); Long leaseId = intent.getLongExtra("lease_id", 0);
Ihr wisst vielleicht was jetzt kommt: Wir könnt einfach prüfen, ob eine leaseId übergeben wurde. Wenn dies der Fall ist, dann laden wir den Lease aus der Datenbank:
if(leaseId != 0 ) { DaoSession daoSession = ((LeaseApplication) getApplicationContext()).getDaoSession(); LeaseDao leaseDao = daoSession.getLeaseDao(); lease = leaseDao.load(leaseId); }
Wenn eine ID übergeben wurde, dann haben wir eine vollständige Entität in unserem Attribut, also fehlt nur noch ein Schritt, um diese auch anzuzeigen… die Werte müssen in die Felder in der Oberfläche eingefügt werden:
if(lease != null) { editTextItem.setText(lease.getItem()); editTextComment.setText(lease.getComment()); }
Ihr könnt bestimmt schon erkennen um was es geht: Wir müssen an kaum einer Stelle noch direkt mit unserer Datenbank arbeiten: kein SQL, nichts!
Wenn ihr schon ganz normal mit SQLite gearbeitet habt, dann wisst ihr, was ich meine. Jetzt könnt ihr auch erkennen, dass sich die Arbeit aus Teil 1 auszahlt (besonders wenn viel aus der DB gelesen und in die DB geschrieben werden soll).
Ich hoffe das Durchreichen der ID war nicht zu viel und eure Apps funktionieren – in Teil 4 werden in Kürze die noch fehlenden Operationen folgen. Wie immer, wenn ihr Fragen habt bitte gern kommentieren.