Eine Bundesligatabelle als Android Content Provider – Teil 3: Browsing

1. Aufgabe

In der dritten und letzten Folge dieser Trilogie zum Thema „Android Content Provider“ geht es um den lesenden Zugriff auf die Daten des Providers. Am vorliegenden Beispiel sollen sowohl die realen Daten in Gestalt der beiden Tabellen Teams und Games ausgelesen werden wie auch die virtuelle Tabelle für die Ligatabelle eines Spielbetriebs. Im Detail sollen Mannschaften und Spielergebnisse (alle bzw. die eines bestimmten Spieltages) und die Ligatabelle (aktuelle Tabelle oder wiederum die eines bestimmten Spieltags) aus dem Content Provider ausgelesen und an der Benutzeroberfläche entsprechend dargestellt werden.

Für die Oberfläche der Hauptaktivität finden Sie einen Vorschlag in Abbildung 1 vor. Sie ist sehr einach gehalten und besitzt für die unterschiedlichen Leseaufträge eine entsprechende Schaltfläche. Für Leseaufträge, die sich auf einen bestimmten Spieltag beziehen, ist am unteren Rand ein Spinner-Steuerelement hinzugefügt. Benötigt ein Leseauftrag einen Index (etwa n.-te Mannschaft, n.-ter Spieltag, usw.), ist dieser vor der Selektion der Schaltfläche am Spinner-Steuerelement einzustellen.

Einstiegsoberfläche der LeagueData Content Viewer App.

Abbildung 1. Einstiegsoberfläche der LeagueData Content Viewer App.


Für die Sub-Aktivitäten „View all Teams“ und „View all Games“ finden Sie Screenshots meiner Realisierung in Abbildung 2 und Abbildung 3 vor:

Oberfläche der Aktivität View all Teams.

Abbildung 2. Oberfläche der Aktivität „View all Teams“.


Oberfläche der Aktivität View all Games.

Abbildung 3. Oberfläche der Aktivität „View all Games“.


Um die Spielergebnisse eines bestimmten Spieltags zu betrachten, gibt es die Aktivität „View Games of Matchday“. Der Einfachheit halber besitzt diese im oberen Teil ein eigenes Spinner-Steuerelement, um den gewünschten Spieltag auszuwählen (Abbildung 4):

Oberfläche der Aktivität View Games of Matchday.

Abbildung 4. Oberfläche der Aktivität „View Games of Matchday“.


Für die Betrachtung der Tabelle an einem bestimmten Spieltag habe ich einen anderen Ansatz gewählt. Der gewünschte Spieltag ist zunächst im User Interface der Hauptaktivität zu selektieren (siehe Abbildung 1), bevor anschließend die Unter-Aktivität aktiviert wird (Abbildung 5). Letzten Endes habe ich diese Entwurfsentscheidung nur getroffen, um die Übergabe von Daten zwischen Aktivitäten innerhalb einer App mit integrieren zu können. Für den Softwareentwurf einer App mit Zielsetzung Produktivbetrieb wäre eine konsistente Bedienoberfläche natürlich anstrebenswert.

Oberfläche der Aktivität View League Table of Matchday.

Abbildung 5. Oberfläche der Aktivität „View League Table of Matchday“.


Nebenbemerkung: Die Oberfläche der App in Abbildung 5 legt den Eindruck nahe, dass die Tabelle in Teilen falsch berechnet wurde (die Anzahl der Spiele weist nicht durchgängig den Wert 2 auf). Dieser Eindruck täuscht bzw. ist dem Umstand geschuldet, dass auf Grund von Spielverschiebungen einige Mannschaften am 2. Spieltag weniger oder bereits mehr Spiele absolviert hatten.

Damit fehlt nur noch die Betrachtung der wichtigsten Aktivität: „Visualisierung der aktuellen Ligatabelle“. Sie unterscheidet sich bzgl. ihrer Oberfläche in nichts von Abbildung 5, nur dass der Inhalt der Tabelle alle vorhandenen Spiele aufweist:

Oberfläche der Aktivität View League Table.

Abbildung 6. Oberfläche der Aktivität „View League Table“.

2. Lösung

Zunächst beginnen wir mit der Betrachtung der Hauptaktivität. Im Großen und Ganzen besitzt sie nur eine Reihe von Schaltflächen, die den jeweiligen Auftrag an eine unterlagerte Aktivität weiterreichen. Da manche der Unteraktivitäten einen (ganzzahligen) Parameter benötigen (welcher Spieltag, welches Spiel, welche Mannschaft, ...), kann man an diesem Beispiel die Versorgung einer Aktivität mit Werten gut studieren. Wir benötigen für die Hauptaktivität ein User Interface (Listing 1) samt Logik-Datei (Listing 2) und eine Ressource-Datei für ausgelagerte Zeichenketten:

01: <RelativeLayout 
02:     xmlns:android="http://schemas.android.com/apk/res/android"
03:     xmlns:tools="http://schemas.android.com/tools"
04:     android:layout_width="match_parent"
05:     android:layout_height="match_parent"
06:     android:orientation="vertical" >
07: 
08:    <Button
09:         android:id="@+id/button_view_all_teams"
10:         android:layout_width="match_parent"
11:         android:layout_height="wrap_content"
12:         android:text="@string/view_all_teams" />
13:    
14:    <Button
15:         android:id="@+id/button_view_all_games"
16:         android:layout_width="match_parent"
17:         android:layout_height="wrap_content"
18:         android:layout_below="@+id/button_view_all_teams"
19:         android:text="@string/view_games" />
20:    
21:    <Button
22:         android:id="@+id/button_view_games_of_matchday"
23:         android:layout_width="match_parent"
24:         android:layout_height="wrap_content"
25:         android:layout_below="@+id/button_view_all_games"
26:         android:text="@string/view_games_of_matchday" />
27:    
28:    <Button
29:         android:id="@+id/button_view_league_table"
30:         android:layout_width="match_parent"
31:         android:layout_height="wrap_content"
32:         android:layout_below="@+id/button_view_games_of_matchday"
33:         android:text="@string/view_league_table" />
34:    
35:    <Button
36:         android:id="@+id/button_view_league_table_of_matchday"
37:         android:layout_width="match_parent"
38:         android:layout_height="wrap_content"
39:         android:layout_below="@+id/button_view_league_table"
40:         android:text="@string/view_league_table_of_matchday" />
41:    
42:     <Button
43:         android:id="@+id/button_view_single_team"
44:         android:layout_width="match_parent"
45:         android:layout_height="wrap_content"
46:         android:layout_below="@+id/button_view_league_table_of_matchday"
47:         android:text="@string/button_view_single_team" /> 
48:         
49:     <Button
50:         android:id="@+id/button_view_single_game"
51:         android:layout_width="match_parent"
52:         android:layout_height="wrap_content"
53:         android:layout_below="@+id/button_view_single_team"
54:         android:text="@string/button_view_single_game" /> 
55:         
56:    <RelativeLayout 
57:         android:id="@+id/InnerRelativeLayout"
58:         android:layout_width="wrap_content"
59:         android:layout_height="wrap_content"
60:         android:layout_alignParentBottom="true" >
61:         
62:         <View
63:             android:id="@+id/viewSeperator"
64:             android:layout_width="fill_parent"
65:             android:layout_height="1dip"
66:             android:background="#FF000000"
67:             android:layout_marginBottom="2dip" />       
68:         
69:         <TextView        
70:             android:id="@+id/textViewSpinnerHeader"
71:             android:layout_width="match_parent"
72:             android:layout_height="wrap_content"
73:             android:layout_below="@+id/viewSeperator"
74:             android:padding="10dp"
75:             android:textSize="18sp" />
76:         
77:        <Spinner
78:             android:id="@+id/spinnerMatchday"
79:             android:layout_width="fill_parent"
80:              android:layout_height="wrap_content"
81:             android:layout_below="@+id/textViewSpinnerHeader"
82:             android:textSize="12sp" />
83:         
84:         <TextView        
85:             android:id="@+id/textViewStatus"
86:             android:layout_width="match_parent"
87:             android:layout_height="wrap_content"
88:             android:layout_below="@+id/spinnerMatchday"
89:             android:maxLines="2"
90:             android:padding="10dp"
91:             android:textSize="18sp" />
92:     
93:     </RelativeLayout>   
94: 
95: </RelativeLayout>

Beispiel 1. Datei activity_main.xml: UI der Hauptaktivität (Content Browser App).


Die Zeichenketten-Ressourcen sind dabei so definiert:

01: <?xml version="1.0" encoding="utf-8"?>
02: 
03: <resources>
04: 
05:     <string name="app_name">LeagueData: Content Viewer</string>
06:     <string name="hello_world">Hello world!</string>
07:     <string name="action_settings">Settings!</string>
08:     
09:     <string name="view_all_teams">View all Teams</string>
10:     <string name="view_games">View all Games</string>
11:     <string name="view_games_of_matchday">View Games of Matchday</string>
12:     <string name="view_league_table">View League Table</string>
13:     <string name="view_league_table_of_matchday">View League Table of Matchday</string>
14:     
15:     <string name="title_activity_team">LeagueData Content Viewer: Table of Teams</string>
16:     <string name="title_activity_games">LeagueData Content Viewer: Table of Games</string>
17:     <string name="title_activity_games_of_matchday">LeagueData Content Viewer: Table of Games (Matchday)</string>
18:     <string name="title_activity_league_table">LeagueData Content Viewer: League Table</string>
19:     
20:     <string name="textMatchDay">"Spiel- \n tag"</string>
21:     <string name="textMatchDaySingleLine">"Spieltag"</string>
22:     
23:     <string name="textTeamHome">Heim</string>
24:     <string name="textTeamAway">Gast</string>
25:     <string name="textResult">Ergebnis</string>
26:     
27:     <string name="button_view_single_team">View single Team</string>
28:     <string name="button_view_single_game">View single Game</string>
29:     
30: </resources>

Fehlt noch die Java-Datei für den Logik-Anteil (Listing 2):

001: package com.example.leaguedataviewer;
002: 
003: import com.example.leaguedataprovider.LeagueDataContract;
004: 
005: import android.view.LayoutInflater;
006: import android.view.View;
007: import android.view.View.OnClickListener;
008: import android.widget.AdapterView;
009: import android.widget.ArrayAdapter;
010: import android.widget.Button;
011: import android.widget.LinearLayout;
012: import android.widget.Spinner;
013: import android.widget.TextView;
014: import android.widget.AdapterView.OnItemSelectedListener;
015: import android.app.Activity;
016: import android.content.ContentResolver;
017: import android.content.Context;
018: import android.content.Intent;
019: import android.content.SharedPreferences;
020: import android.database.Cursor;
021: import android.net.Uri;
022: import android.os.Bundle;
023: import android.preference.PreferenceManager;
024: 
025: import java.util.ArrayList;
026: import java.util.List;
027: 
028: public class MainActivity extends Activity implements OnClickListener, OnItemSelectedListener {
029: 
030:     private static final String SPINNER_INDEX = "SPINNER_INDEX";
031:     public  static final String BUNDLE_MATCHDAY_NAME = "MATCH_DAY";
032:     
033:     public static String resultColumns[];
034:     public static int[] resourceIDs;
035:     
036:     private Spinner spinnerMatchday;
037:     private TextView textViewStatus;    
038:     private int selectedMatchDay;
039:     
040:     public MainActivity() {
041:         super();
042:         System.out.println ("c'tor MainActivity() LeagueData Viewer");
043:     }
044:     
045:     static {
046:         System.out.println ("static c'tor MainActivity League Data Viewer ...");
047:                 
048:         resultColumns = new String[] {
049:             LeagueDataContract.Table.TEAMNAME,
050:             LeagueDataContract.Table.GAMESPLAYED,
051:             LeagueDataContract.Table.WINS,
052:             LeagueDataContract.Table.DRAWS,
053:             LeagueDataContract.Table.LOSSES,
054:             LeagueDataContract.Table.GSELF,
055:             LeagueDataContract.Table.GOTHER,
056:             LeagueDataContract.Table.GDIFF,
057:             LeagueDataContract.Table.POINTS
058:         };
059:    
060:         resourceIDs = new int[] {
061:             R.id.row_team_name,
062:             R.id.row_games_played,
063:             R.id.row_wins,
064:             R.id.row_draws,
065:             R.id.row_losses,
066:             R.id.row_gself,
067:             R.id.row_gother,
068:             R.id.row_gdiff,
069:             R.id.row_points
070:         };        
071:     }
072:     
073:     @Override
074:     protected void onCreate(Bundle savedInstanceState) {
075:         super.onCreate(savedInstanceState);
076:         
077:         this.setContentView(R.layout.activity_main);
078:         
079:         // connect event handler
080:         Button b = (Button) this.findViewById(R.id.button_view_all_teams);
081:         b.setOnClickListener(this);    
082:         b = (Button) this.findViewById(R.id.button_view_all_games);
083:         b.setOnClickListener(this);
084:         b = (Button) this.findViewById(R.id.button_view_games_of_matchday);
085:         b.setOnClickListener(this);
086:         b = (Button) this.findViewById(R.id.button_view_league_table);
087:         b.setOnClickListener(this);
088:         b = (Button) this.findViewById(R.id.button_view_league_table_of_matchday);
089:         b.setOnClickListener(this);
090:         b = (Button) this.findViewById(R.id.button_view_single_team);
091:         b.setOnClickListener(this);
092:         b = (Button) this.findViewById(R.id.button_view_single_game);
093:         b.setOnClickListener(this);
094:         
095:         this.textViewStatus = (TextView) this.findViewById(R.id.textViewStatus);
096:         this.textViewStatus.setText("LeagueData: Content Admin started ...");
097:         
098:         // setup spinner widget
099:         this.selectedMatchDay = -1;
100:         this.spinnerMatchday = (Spinner) this.findViewById(R.id.spinnerMatchday);
101:         MainActivity.fillSpinnerMatchDays(this, this.spinnerMatchday, 49);
102:         this.spinnerMatchday.setOnItemSelectedListener (this);  // fires onItemSelected (!)    
103:         
104:         // retrieve index of spinner's selected item, if any 
105:         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
106:         this.selectedMatchDay = preferences.getInt(SPINNER_INDEX, -1);
107:         System.out.println("onCreate: SharedPreferences: this.selectedMatchDay = " + this.selectedMatchDay);
108:         if (this.selectedMatchDay != -1) {
109:             this.spinnerMatchday.setSelection(this.selectedMatchDay - 1);
110:         }
111:     }
112:     
113:     @Override
114:     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
115: 
116:         int selected = this.spinnerMatchday.getSelectedItemPosition();
117:         this.selectedMatchDay = selected + 1;    
118:     }
119:     
120:     @Override
121:     protected void onPause() {
122:         super.onPause();
123:         
124:         // want to store selected index of 'matchday' spinner
125:         System.out.println("onPause ...");
126:         this.selectedMatchDay = this.spinnerMatchday.getSelectedItemPosition() + 1;
127:         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
128:         SharedPreferences.Editor ed = preferences.edit();
129:         ed.putInt(SPINNER_INDEX, this.selectedMatchDay);
130:         ed.commit(); 
131:     }
132: 
133:     @Override
134:     public void onClick(View v) {
135:         
136:         String caption = (String) ((Button) v).getText();
137:         System.out.println ("onClick: " + caption);
138:         
139:         switch(v.getId()) {
140:         case R.id.button_view_all_teams:
141:             Intent teamsActivityIntent = new Intent(this, TeamsActivity.class);
142:             this.startActivity(teamsActivityIntent);              
143:             break;
144:         case R.id.button_view_all_games:
145:             Intent gamesIntent = new Intent(this, GamesActivity.class);
146:             this.startActivity(gamesIntent);  
147:             break;
148:         case R.id.button_view_games_of_matchday:
149:             Intent gamesOfMatchdayIntent = new Intent(this, GamesOfMatchdayActivity.class);
150:             this.startActivity(gamesOfMatchdayIntent);
151:             break;
152:         case R.id.button_view_league_table:
153:             Intent leagueTableIntent = new Intent(this, LeagueTableActivity.class);
154:             leagueTableIntent.putExtra(BUNDLE_MATCHDAY_NAME, String.valueOf(-1));
155:             this.startActivity(leagueTableIntent);            
156:             break;
157:         case R.id.button_view_league_table_of_matchday:
158:             leagueTableIntent = new Intent(this, LeagueTableActivity.class);
159:             leagueTableIntent.putExtra(BUNDLE_MATCHDAY_NAME, String.valueOf(this.selectedMatchDay));
160:             this.startActivity(leagueTableIntent);            
161:             break;
162:         case R.id.button_view_single_team:
163:             this.viewSingleTeam(this.selectedMatchDay);
164:             break;
165:         case R.id.button_view_single_game:
166:             this.viewSingleGame(this.selectedMatchDay);            
167:             break;
168:         default:
169:             System.out.println ("onClick: Internal Error: Wrong Id");
170:         }
171:     }
172: 
173:     @Override
174:     public void onNothingSelected(AdapterView<?> parent) {
175:     }
176:         
177:     // helper methods
178:     private void viewSingleTeam(int id) {
179:         
180:         ContentResolver cr = this.getContentResolver();
181:         
182:         String resultColumns[] = new String[] {
183:             LeagueDataContract.Teams.NAME,
184:             LeagueDataContract.Teams.CITY
185:         };
186: 
187:         Uri teams = Uri.parse(LeagueDataContract.CONTENT_TEAMS);
188:         Uri teamWithId = Uri.withAppendedPath(teams, String.valueOf(id));
189:         
190:         // retrieve single ream record
191:         Cursor cursor = cr.query(teamWithId, resultColumns, null, null, null);
192:         
193:         if (cursor == null) {
194:             System.out.println ("  Internal ERROR: Cursor IS NULL");
195:         }
196:         else if (! cursor.moveToFirst()) {
197:             System.out.println("  No content currently !");
198:         }
199:         else {
200:             
201:             int nameColIndex = cursor.getColumnIndex (LeagueDataContract.Teams.NAME);
202:             int cityColIndex = cursor.getColumnIndex (LeagueDataContract.Teams.CITY);
203:             String name = cursor.getString (nameColIndex);
204:             String city = cursor.getString (cityColIndex);
205:             
206:             this.textViewStatus.setText (
207:                 "Table Teams:  Row " + id + ": " + name + " (" + city + ")");
208:         }
209:             
210:         if (cursor != null)
211:             cursor.close();
212:     }
213:     
214:     private void viewSingleGame(int id) {
215:         
216:         ContentResolver cr = this.getContentResolver();
217:         
218:         String resultColumns[] = new String[] {
219:             LeagueDataContract.Games.MATCHDAY,
220:             LeagueDataContract.Games.TEAMHOME,
221:             LeagueDataContract.Games.TEAMAWAY,
222:             LeagueDataContract.Games.GOALSHOME,
223:             LeagueDataContract.Games.GOALSAWAY
224:         };
225: 
226:         Uri uriGames = Uri.parse(LeagueDataContract.CONTENT_GAMES);
227:         Uri uriGameWithId = Uri.withAppendedPath(uriGames, String.valueOf(id));
228:         
229:         // retrieve single ream record
230:         Cursor cursor = cr.query(uriGameWithId, resultColumns, null, null, null);
231:         
232:         if (cursor == null) {
233:             System.out.println ("  Internal ERROR: Cursor IS NULL");
234:         }
235:         else if (! cursor.moveToFirst()) {
236:             System.out.println("  No content currently !");
237:         }
238:         else {
239:             
240:             int matchDayColIndex = cursor.getColumnIndex (LeagueDataContract.Games.MATCHDAY);
241:             int teamHomeColIndex = cursor.getColumnIndex (LeagueDataContract.Games.TEAMHOME);
242:             int teamAwayColIndex = cursor.getColumnIndex (LeagueDataContract.Games.TEAMAWAY);
243:             int goalsHomeColIndex = cursor.getColumnIndex (LeagueDataContract.Games.GOALSHOME);
244:             int goalsAwayColIndex = cursor.getColumnIndex (LeagueDataContract.Games.GOALSAWAY);
245:             
246:             int matchDay = cursor.getInt (matchDayColIndex);
247:             int teamHome = cursor.getInt (teamHomeColIndex);
248:             int teamAway = cursor.getInt (teamAwayColIndex);
249:             int goalsHome = cursor.getInt (goalsHomeColIndex);
250:             int goalsAway = cursor.getInt (goalsAwayColIndex);         
251:             
252:             this.textViewStatus.setText (
253:                 "Table Games: Row " + id + ": MatchDay=" + matchDay + ".\n");
254:             
255:             this.textViewStatus.append("TeamHome=" + teamHome + ", TeamAway=" + teamAway +
256:                     ", GoalsHome=" + goalsHome + ", GoalsAway=" + goalsAway);
257:         }
258:             
259:         if (cursor != null)
260:             cursor.close();
261:     }
262:     
263:     // public class helper methods
264:     public static ArrayList<String> retrieveNamesOfTeams (ContentResolver cr) {
265:         
266:         ArrayList<String> namesOfTeams = new ArrayList<String>();
267:         String resultColumns[] = new String[] { LeagueDataContract.Teams.NAME };
268:         Uri uriTeams = Uri.parse(LeagueDataContract.CONTENT_TEAMS);
269:         Cursor cursor = cr.query(uriTeams, resultColumns, "", null, "");
270: 
271:         if (cursor == null) {
272:             System.out.println ("  Internal ERROR: Cursor IS NULL");
273:         }
274:         else if (! cursor.moveToFirst()) {
275:             System.out.println("  No content currently !");
276:         }
277:         else {
278:             // retrieve names of teams
279:             int nameColIndex = cursor.getColumnIndex (LeagueDataContract.Teams.NAME);
280:             do {
281:                 String name = cursor.getString (nameColIndex);
282:                 namesOfTeams.add(name);
283:             }
284:             while (cursor.moveToNext());
285:         }
286:             
287:         if (cursor != null)
288:             cursor.close();
289:         
290:         return namesOfTeams;
291:     }
292:     
293:     public static void fillSpinnerMatchDays (Context context, Spinner spinner, int numMatchDays) {
294:         
295:         List<String> matchDays = new ArrayList<String>();
296:         for (int i = 0; i < numMatchDays; i ++) {
297:             Integer n = Integer.valueOf(i + 1);
298:             matchDays.add(n.toString());
299:         }
300: 
301:         // poke list of names into spinner widget
302:         ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(
303:             context, android.R.layout.simple_list_item_1, matchDays);
304:         spinner.setAdapter(arrayAdapter);
305:     }    
306:     
307:     public static void inflateDataGridRow (Activity activity, LayoutInflater inflater) {
308:         
309:         LinearLayout headerRow = (LinearLayout) inflater.inflate(R.layout.league_table_row, null);
310:         LinearLayout headerContainer = (LinearLayout) activity.findViewById(R.id.headerAnchor);
311:         headerContainer.addView(headerRow);
312:         
313:         TextView textView = (TextView) headerRow.findViewById(R.id.row_team_name);
314:         textView.setText("Mannschaft");
315:         textView = (TextView) headerRow.findViewById(R.id.row_number);
316:         textView.setText("Platz");    
317:         textView = (TextView) headerRow.findViewById(R.id.row_games_played);
318:         textView.setText("Spiele");
319:         textView = (TextView) headerRow.findViewById(R.id.row_wins);
320:         textView.setText("S");
321:         textView = (TextView) headerRow.findViewById(R.id.row_draws);
322:         textView.setText("U");
323:         textView = (TextView) headerRow.findViewById(R.id.row_losses);
324:         textView.setText("N");
325:         textView = (TextView) headerRow.findViewById(R.id.row_gself);
326:         textView.setText("Tore");
327:         textView = (TextView) headerRow.findViewById(R.id.row_gother);
328:         textView.setText("Tore");
329:         textView = (TextView) headerRow.findViewById(R.id.row_gdiff);
330:         textView.setText("Diff.");
331:         textView = (TextView) headerRow.findViewById(R.id.row_points);
332:         textView.setText("Punkte");
333:     }
334:     
335:     public static int getNumberOfTeams (ContentResolver cr) {
336:         
337:         Uri uriTeams = Uri.parse(LeagueDataContract.CONTENT_TEAMS);
338:         Uri uriTeamsCount = Uri.withAppendedPath(uriTeams, "/count");
339:         
340:         Cursor cursor = cr.query(uriTeamsCount, null, null, null, null);
341:         
342:         int rows = -1;
343:         if (cursor == null) {
344:             System.out.println ("Internal ERROR: Cursor IS NULL");
345:         }
346:         else if (! cursor.moveToFirst()) {
347:             System.out.println("  No content currently !");
348:         }
349:         else {
350:             rows = cursor.getInt(0);
351:             cursor.close();
352:         }
353: 
354:         return rows;
355:     }
356: }

Beispiel 2. Datei MainActivity.java: Logik-Anteil der Hauptaktivität (Content Browser App).


Werfen Sie bitte in Listing 2 einen Blick auf die Zeilen 152 bis 161: Hier wird zum Betrachten der Ligatabelle in beiden Fällen (Tabelle zu einem bestimmten Spieltag oder die aktuelle Tabelle) jeweils die dieselbe Unteraktivität (Klasse LeagueTableActivity) aktiviert. Die Information, welche Tabelle berechnet und zur Anzeige gebracht werden soll, wird mittels eines „Parameters“ durch die gerufene Aktivität ermittelt. Generell gibt es an der Klasse Intent eine Methode putExtra, die, wie der Name schon zum Ausdruck bringt, ein Wertepaar Schlüssel/Wert entgegennimmt. Der Wert -1 teilt in meiner Realisierung der gerufenen Aktivität mit, dass die aktuelle Ligatabelle gefragt ist. Ein anderer Wert würde den Spieltag spezifizieren, dessen Ligatabelle zu berechnen ist.

Die Klasse LeagueTableActivity stellt neben ihrer eigentlichen Funktionalität auch einige Hilfsmethoden für die anderen Aktivitäten des Projekts zur Verfügung. Das wäre zum Beispiel die Methode retrieveNamesOfTeams (Zeilen 264 bis 291), um an unterschiedlichen Stellen im Projekt die Namen der beteiligten Mannschaften zur Verfügung zu haben. Ähnlich verhält es sich mit der Methode getNumberOfTeams in den Zeilen 335 bis 355 von Listing 2. Auch die Anzahl der Mannschaften ist immer wieder von Interesse.

Die Klasse LeagueTableActivity besitzt im gesamten Projekt eine vergleichsweise anspruchvolle Realisierung, wir stellen Sie deshalb in den Mittelpunkt unserer Betrachtungen und Erläuterungen. Um den Aufwand in der Realisierung auf ein Minimum zu beschränken, sollen die beiden Klassen SimpleCursorAdapter und GridView zum Einsatz kommen. Mit Unterstützung des LeagueDataProvider Content Providers ist dies zunächst einmal auch kein Problem. Im Gegensatz zu den anderen Beispielen dieses Projekts wollen wir dieses Mal die Last in der onCreate-Methode der dazugehörigen Aktivität gering halten. Dies bedeutet, dass die Erzeugung des gesuchten Cursor-Objekts asynchron in Sekundärthreads zu erfolgen hat. Dazu gibt es Android Hilfsmittel in Gestalt einer CursorLoader-Klasse, die im UI-Thread der Aktivität die Abfrage an den Content Provider nur anstößt, seine eigentliche Rechenarbeit aber in den Kontext von Hilfsthreads auslagert. Die Resultate der Berechungen werden wiederum im Aufruf von Callback-Methoden zurückgeliefert. Aus diesem Grund implementiert die LeagueTableActivity-Klasse die LoaderManager.LoaderCallbacks<Cursor>-Schnittstelle, was bedeutet, dass die Methoden onCreateLoader, onLoadFinished und onLoaderReset vorhanden sein müssen.

Angestoßen wird der asynchrone Bearbeitungsvorgang in den Zeilen 113 und 114 von Listing 4: Es wird eine Instanz der LoaderManager-Klasse angelegt, durch den Aufruf der initLoader-Methode an diesem Objekt kommen die Callback-Methoden ins Spiel. Als erste wird die onCreateLoader-Methode gerufen. Sie dient nur dem Zweck, ein CursorLoader-Objekt zusammenzustellen. Dieses enthält im Wesentlichen eine URI und alle weiteren Parameter, um einen query-Aufruf an den Content Provider (asynchron) absetzen zu können.

Die Resultate des query-Aufrufs an den Content Provider werden der Aktivität in der onLoadFinished-Methode übertragen. Endlich kann in Zeile 147 das Cursor-Objekt an das SimpleCursorAdapter-Objekt weitergereicht werden (Aufruf von swapCursor). Auf Grund der Verknüpfung des SimpleCursorAdapter-Objekts in Zeile 105 mit einem GridView-Objekt kann dieses die im Cursor-Objekt vorhandenen Resultate zur Anzeige bringen.

Soweit so gut, wir sind schon fast am Ende dieser Betrachtungen angekommen, gäbe es da nicht noch ein paar kleine Stolpersteine. Der erste Stolperstein wird durch das SimpleCursorAdapter-Objekt verursacht. Dieses erwartet, dass jede Resultat-Reihe des zu Grunde liegenden Cursor-Objekts eine Spalte mit dem Namen _id besitzt. In vielen Abfragen stellt dies auch gar kein Problem dar, da fast alle SQL-Tabellen einen Primärschlüssel besitzen, dessen Name sich mit einer SQL-AS Direktive auf den Bezeichner _id umlenken lässt. Ausgerechnet in unserer Fallstudie bereitet dies mir einiges Kopfzerbrechen, da ich zur Berechnung der Ligatabelle mit einem geschachtelten SQL-SELECT (mit GROUP BY Direktive) nicht ohne weiteres in der Lage bin, eine zusätzliche _id-Spalte generieren zu lassen. Um es ehrlicher zu formulieren: Meine SQL-Kenntnisse sind zu schwach, um den vorhandenen Ausdruck noch dementsprechend zu erweitern. Da ich aber auf keinen Fall auf die Funktionalität der SimpleCursorAdapter-Klasse verzichten wollte, habe ich die Spalte TEAMID aus der Tabelle TABLES zu diesem Zweck missbraucht (also auf den Alias-Namen _id abgebildet). Damit akzeptiert das SimpleCursorAdapter-Objekt das generierte Cursor-Objekt (es ist eine Spalte mit dem Namen _id vorhanden). Allerdings spiegelt diese Spalte nicht die korrekte Reihenfolge der Mannschaften in der Ligatabelle wieder, so dass ich diese Spalte im GridView-Objekt nicht zur Ansicht bringen kann.

Wie schaffe ich es nun, schließlich doch noch eine Nummerierung der Gestalt „1.“, „2.“, „3.“ etc. dem GridView-Objekt hinzuzufügen? Das sollte doch nicht wirklich ein großes Problem sein, habe ich mir an dieser Stelle gedacht. Die query-Abfrage an den Content Provider fordert (in meiner Implementierung) neun Spalten explizit an:

resultColumns = new String[] {
    "TEAMNAME",
    "GAMESPLAYED",
    "WINS",
    "DRAWS",
    "LOSSES",
    "GSELF",
    "GOTHER",
    "GDIFF",
    "POINTS"
};

Das Adapter-Objekt wiederum besitzt zur Beschreibung einer einzelnen Reihe eine .XML-Vorlage league_table_row.xml, die zehn TextView-Objekte besitzt, also ein zusätzliches TextView-Objekt, in dem nachträglich die Rangnummer der Mannschaften ergänzt werden kann (Listing 3):

 

001: <?xml version="1.0" encoding="utf-8"?>
002: 
003: <LinearLayout 
004:     xmlns:android="http://schemas.android.com/apk/res/android"
005:     android:id="@+id/league_table_row"
006:     android:layout_width="match_parent"
007:     android:layout_height="match_parent" 
008:     android:orientation="horizontal">
009:     
010:     <TextView
011:          android:id="@+id/row_number"
012:          android:layout_width="0px"
013:          android:layout_weight="12"
014:          android:layout_height="wrap_content"
015:          android:textSize="14sp"
016:          android:layout_marginLeft="6dp"
017:          android:layout_marginBottom="6dp" />
018:         
019:     <TextView
020:          android:id="@+id/row_team_name"
021:          android:layout_width="0px"
022:          android:layout_weight="45"
023:          android:layout_height="wrap_content"
024:          android:textSize="14sp"
025:          android:layout_marginLeft="6dp"
026:          android:layout_marginBottom="6dp" />
027:     
028:     <TextView
029:          android:id="@+id/row_games_played"
030:          android:layout_width="0px"
031:          android:layout_weight="12"
032:          android:layout_height="wrap_content"
033:          android:textSize="14sp"
034:          android:layout_marginLeft="6dp"
035:          android:layout_marginBottom="6dp" />
036:     
037:     <TextView
038:          android:id="@+id/row_wins"
039:          android:layout_width="0px"
040:          android:layout_weight="5"
041:          android:layout_height="wrap_content"
042:          android:textSize="14sp"
043:          android:layout_marginLeft="6dp"
044:          android:layout_marginBottom="6dp" />
045:     
046:     <TextView
047:          android:id="@+id/row_draws"
048:          android:layout_width="0px"
049:          android:layout_weight="5"
050:          android:layout_height="wrap_content"
051:          android:textSize="14sp"
052:          android:layout_marginLeft="6dp"
053:          android:layout_marginBottom="6dp" />
054:        
055:     <TextView
056:          android:id="@+id/row_losses"
057:          android:layout_width="0px"
058:          android:layout_weight="5"
059:          android:layout_height="wrap_content"
060:          android:textSize="14sp"
061:          android:layout_marginLeft="6dp"
062:          android:layout_marginBottom="6dp" />
063:            
064:     <TextView
065:          android:id="@+id/row_gself"
066:          android:layout_width="0px"
067:          android:layout_weight="10"
068:          android:layout_height="wrap_content"
069:          android:textSize="14sp"
070:          android:textAlignment="viewEnd"
071:          android:layout_marginLeft="6dp"
072:          android:layout_marginBottom="6dp" />
073:     
074:     <TextView
075:          android:id="@+id/row_gother"
076:          android:layout_width="0px"
077:          android:layout_weight="8"
078:          android:layout_height="wrap_content"
079:          android:textSize="14sp"
080:          android:textAlignment="viewEnd"
081:          android:layout_marginLeft="6dp"
082:          android:layout_marginBottom="6dp" />
083:           
084:     <TextView
085:          android:id="@+id/row_gdiff"
086:          android:layout_width="0px"
087:          android:layout_weight="8"
088:          android:layout_height="wrap_content"
089:          android:textSize="14sp"
090:          android:layout_marginLeft="6dp"
091:          android:layout_marginBottom="6dp" />
092: 
093:     <TextView
094:          android:id="@+id/row_points"
095:          android:layout_width="0px"
096:          android:layout_weight="12"
097:          android:layout_height="wrap_content"
098:          android:textSize="14sp"
099:          android:layout_marginLeft="6dp"
100:          android:layout_marginBottom="6dp" />
101:                 
102: </LinearLayout>

Beispiel 3. Datei league_table_row.xml: Beschreibung einer Ligatabellenreihe.

 

Der Zugriff auf die einzelnen View-Elemente in einem GridView-Objekt ist nicht ganz trivial, mit den beiden Methoden getChildCount und getChildAt ist es aber zu schaffen. Mit diesen Erkenntnissen dachte ich nun, das Ziel erreicht zu haben. Im „Portrait “-Modus lief die App auch wunderbar – solange, bis ich zufälligerweise die App im „Landscape“-Modus ausführte. In diesem Modus zeigte die App (auf meinem Smartphone) von der Ligatabelle (18 Mannschaften) nur die ersten 16 Mannschaften (korrekt) an. Um die letzten beiden Mannschaften zur Ansicht zu bringen, platziert man das GridView-Objekt innerhalb eines ScrollView-Elements, und kann dann durch Benutzung des Schiebebalkens alle Teile der Tabelle in den sichtbaren Bereich der App verschieben. Soweit so gut, nur wurden dann in der ersten Spalte nicht – um im Beispiel zu bleiben – am unteren Ende die Rangnummern 17 und 18 angezeigt, sondern kleinere (falsche) Werte.

Auf dieses Phänomen müssen wir genauer eingehen. Haben wir es mit einem SQL-Abfrageresultat von 18 Reihen zu tun und befinden sich vom GridView-Objekt nur 16 Reihen im sichtbaren Bereich, dann bedeutet dies unter Android nicht, dass die fehlenden zwei Reihen allokiert, aber eben nur nicht sichtbar sind. Die Realität ist etwas komplizierter: Das GridView-Objekt besitzt (im „Landscape“-Modus) zu jedem Zeitpunkt nur 16 Zeilen (!). Ihr Inhalt wird entsprechend des Scrollings an Hand eines internen Recycling-Verfahrens angepasst, sprich die Inhalte der 16 Reihen werden ausgetauscht und nicht, wie von mir erwartet, durch neue, vormals unsichtbare Elemente im sichtbaren Bereich ersetzt. Unglücklicherweise ist dieser Recycling-Mechanismus so gut wie überhaupt nicht dokumentiert. Wenn überhaupt, kann man via Stackoverflow einige Beobachtungen zu diesem Thema finden, für die es in der offiziellen Android-Dokumentation keine Bestätigung gibt.

Für das Erstellen von Produkt-Software sollte man sich auf derartige Niederungen nicht begeben. Um diese Fallstudie mit all ihren Nebenwirkungen und Stolpersteinen zügig voranzutreiben, gehe ich es ausnahmsweise auf diese Beobachtungen ein und muss folglich, um in der ersten Spalte des GridView-Objekt eine korrekte Rangnummerierung hinzuzufügen, die OnScrollListener-Schnittstelle implementieren und in ihrer onScroll-Methode für eine Zeilennummerierung sorgen – unter Berücksichtigung des internen Recycling-Mechanismus von Android-GridView-Objekten!

001: package com.example.leaguedataviewer;
002: 
003: import com.example.leaguedataprovider.LeagueDataContract;
004: 
005: import android.app.Activity;
006: import android.content.Context;
007: import android.content.CursorLoader;
008: import android.content.Loader;
009: import android.database.Cursor;
010: import android.net.Uri;
011: import android.os.Bundle;
012: import android.view.LayoutInflater;
013: import android.view.View;
014: import android.widget.AbsListView;
015: import android.widget.AbsListView.OnScrollListener;
016: import android.widget.GridView;
017: import android.widget.SimpleCursorAdapter;
018: import android.widget.TextView;
019: import android.app.LoaderManager;
020: 
021: public class LeagueTableActivity extends Activity implements LoaderManager.LoaderCallbacks<Cursor> {
022:     
023:     // the loader's unique id
024:     private static final int LOADER_ID = 1;
025:     
026:     // mapping of a given key to a value
027:     private static final String REQUEST_KEY = "BUNDLE_KEY_REQUESTED_DAY";
028: 
029:     // grid view to visualize a league table
030:     private GridView gridView;
031:     
032:     // list of callback's through which we will interact with the LoaderManager
033:     private LoaderManager.LoaderCallbacks<Cursor> callbacks;
034:     
035:     // adapter that binds the data to the grid view
036:     private SimpleCursorAdapter adapter;
037:     
038:     @Override
039:     protected void onCreate(Bundle savedInstanceState) {
040: 
041:         super.onCreate(savedInstanceState);
042:         this.setContentView(R.layout.activity_league_table);
043:         
044:         // retrieve selected match day
045:         int requestedMatchDay = 0;
046:         Bundle extras = this.getIntent().getExtras();
047:         if (extras != null) {
048:             requestedMatchDay = Integer.parseInt(extras.getString (MainActivity.BUNDLE_MATCHDAY_NAME));              
049:         }
050:         
051:         // setup grid view, especially "first column" issue
052:         this.gridView = (GridView) this.findViewById(R.id.mygridview);
053:         this.gridView.setOnScrollListener(new OnScrollListener() {
054:             
055:             @Override
056:             public void onScrollStateChanged(AbsListView view, int scrollState) {
057:                 // invoked while the grid view is being scrolled - not needed
058:             }
059:             
060:             @Override
061:             public void onScroll(
062:                 AbsListView listView, int firstVisibleItem,
063:                 int visibleItemCount, int totalItemCount) {
064:                 
065:                 LeagueTableActivity.this.gridView.getChildCount();
066:                 
067:                 // take care of grid view's recycling mechanism,
068:                 // therefore just use indices from 0 to visibleItemCount
069:                 for (int i = 0; i < visibleItemCount; i ++) {
070:                     View view = LeagueTableActivity.this.gridView.getChildAt(i);
071:                     if (view != null) {
072:                         TextView firstColumn =
073:                             (TextView) view.findViewById(R.id.row_number);
074:                             firstColumn.setText((i + firstVisibleItem + 1) + ".");
075:                     }
076:                     else {
077:                         System.out.println("onScroll: Unexpected null at index " + i);
078:                     }
079:                 }
080:             }
081:         });
082: 
083:         this.callbacks = null;
084: 
085:         // create header for grid view
086:         LayoutInflater inflater = (LayoutInflater) this.getSystemService (Context.LAYOUT_INFLATER_SERVICE);
087:         MainActivity.inflateDataGridRow(this, inflater); 
088:         
089:         this.fillDataGrid(requestedMatchDay);
090:     }
091: 
092:     // helper methods
093:     private void fillDataGrid (int requestedMatchDay) {
094: 
095:         this.adapter = new SimpleCursorAdapter (
096:             this,                       // context
097:             R.layout.league_table_row,  // resource identifier of a layout file that defines the views for this list item
098:             null,                       // pass the adapter a database cursor only when the data has finished loading
099:             MainActivity.resultColumns, // list of column names representing the data to bind to the UI
100:             MainActivity.resourceIDs,   // views that should display column in the "from" parameter
101:             0                           // prevent adapter from registering a ContentObserver 
102:         );
103:         
104:         // associate the (now empty) adapter with the grid view
105:         this.gridView.setAdapter(adapter);   
106:         
107:         // register this object as the loader manager's callback object
108:         this.callbacks = this;
109: 
110:         // create loader ... which schedules  an 'onCreateLoader' invocation
111:         Bundle bundle = new Bundle();
112:         bundle.putInt(REQUEST_KEY, requestedMatchDay);
113:         LoaderManager lm = this.getLoaderManager();
114:         lm.initLoader(LOADER_ID, bundle, this.callbacks);
115:     }
116:     
117:     @Override
118:     public Loader<Cursor> onCreateLoader(int id, Bundle args) {
119: 
120:         int requestedMatchDay = args.getInt(REQUEST_KEY);
121:                 
122:         Uri uriTable = Uri.parse(LeagueDataContract.CONTENT_TABLE);
123:         
124:         CursorLoader cursorLoader;
125:         if (requestedMatchDay >= 1) {
126:             
127:             // match day selected
128:             String[] selectionArgs = new String[] { String.valueOf(requestedMatchDay)};
129:             cursorLoader = new CursorLoader (LeagueTableActivity.this, uriTable, MainActivity.resultColumns, null, selectionArgs, null);            
130:         }
131:         else {
132:             
133:             // no match day selected
134:             cursorLoader = new CursorLoader (LeagueTableActivity.this, uriTable, MainActivity.resultColumns, null, null, null);
135:         }
136: 
137:         return cursorLoader; 
138:     }
139:         
140:     @Override
141:     public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
142:         
143:         // a switch-case is useful when dealing with multiple loaders/Id's
144:         switch (loader.getId()) {
145:             case LOADER_ID:
146:                 // asynchronous load phase is now complete
147:                 this.adapter.swapCursor(cursor);
148:                 break;
149:         }    
150:     }
151: 
152:     @Override
153:     public void onLoaderReset(Loader<Cursor> loader) {
154:     }        
155: }

Beispiel 4. Klasse LeagueTableActivity: Implementierung der Kernfunktionalität.


Mit den geleisteten Vorarbeiten in Listing 3 (Darstellung einer einzelnen Reihe in einer Ligatabelle) und Listing 4 (Berechnung der Ligatabelle) lassen sich zwei Unter-Aktivitäten dieses Projekts software-technisch auf eine Oberfläche abbilden, siehe dazu das nachfolgenden UI-XML-Dokument:

01: <LinearLayout
02:     xmlns:android="http://schemas.android.com/apk/res/android"
03:     android:layout_width="match_parent"
04:     android:layout_height="match_parent"
05:     android:orientation="vertical">
06:     
07:     <LinearLayout
08:         android:id="@+id/headerAnchor"
09:         android:layout_width="fill_parent"
10:         android:layout_height="wrap_content"
11:         android:orientation="vertical"
12:         android:paddingTop="20dp" />
13:     
14:     <View
15:         android:id="@+id/viewSeperator"
16:         android:layout_width="fill_parent"
17:         android:layout_height="1dip"
18:         android:background="#FF000000"
19:         android:layout_marginBottom="10dip" />
20:     
21:     <GridView
22:         android:id="@+id/mygridview"
23:         android:layout_width="fill_parent"
24:         android:layout_height="fill_parent"
25:         android:numColumns="1" 
26:         android:stretchMode="columnWidth"
27:         android:gravity="center" />
28: 
29: </LinearLayout >

Eine Eigenschaft des Content Provider Konzepts sollten wir noch ansprechen: Ein System-Eigenschaft von Android besteht ja darin, in Abhängigkeit von der Auslastung des Android-Betriebssystems bzw. in Bezug auf die Verfügbarkeit von freiem Speicher eigenmächtig zu entscheiden, wann eine App (rsp. der damit verbundene Prozess) terminiert wird. Dabei spielt es für den Client eines Content Providers keine Rolle, ob sein Host Prozess gerade am Laufen ist oder nicht. Greift ein Client auf einen Content Provider zu und ist dieser gerade nicht im Hauptspeicher geladen, so wird dieser einfach nachgestartet. Um dies Beobachten zu können, sind in allen Anwendungen der Fallstudie an zentralen Stellen (Konstruktor der MainActivity-Klasse, onCreate-Methode, ...) ergänzende Testausgaben auf Basis von System.out.println vorhanden. In Abbildung 7 kann man nun gut erkennen, dass bei Aufruf einer query-Methode am Provider ein neuer Host-Prozess kreiert wird, wenn dieser nicht am Laufen ist:

LogCat-Fenster von Eclipse: Nachstarten des Content Provider Host-Prozesses.

Abbildung 7. LogCat-Fenster von Eclipse: Nachstarten des Content Provider Host-Prozesses.


Damit wären wir am Ende der Betrachtungen der etwas anspruchsvolleren Regionen dieser App angekommen. Für die drei noch verbleibenden Aktivitäten TeamsActivity, GamesActivity und GamesOfMatchdayActivity listen wir den Quellcode nachfolgend ohne zusätzliche Kommentierungen auf.

Implementierung der Aktivität „Table of Teams

01: package com.example.leaguedataviewer;
02: 
03: import com.example.leaguedataprovider.LeagueDataContract;
04: 
05: import android.app.Activity;
06: import android.content.ContentResolver;
07: import android.content.Context;
08: import android.database.Cursor;
09: import android.net.Uri;
10: import android.os.Bundle;
11: import android.view.LayoutInflater;
12: import android.widget.LinearLayout;
13: import android.widget.TableLayout;
14: import android.widget.TextView;
15: 
16: public class TeamsActivity extends Activity {
17:     
18:     private TableLayout table;
19: 
20:     @Override
21:     protected void onCreate(Bundle savedInstanceState) {
22:         super.onCreate(savedInstanceState);
23:         setContentView(R.layout.activity_teams);
24:         
25:         // this.textViewTeams = (TextView) this.findViewById(R.id.text_view_teams);
26:         this.table = (TableLayout) this.findViewById(R.id.tableTeamsLayoutAnchor);
27:         this.viewAllTeams();
28:     }
29:     
30:     private void viewAllTeams () {
31:         
32:         // retrieve teams records
33:         ContentResolver cr = this.getContentResolver();
34:         
35:         String resultColumns[] = new String[] {
36:             LeagueDataContract.Teams._ID,
37:             LeagueDataContract.Teams.NAME,
38:             LeagueDataContract.Teams.CITY
39:         };
40:         
41:         Uri uriTeams = Uri.parse(LeagueDataContract.CONTENT_TEAMS);
42:         Cursor cursor = cr.query(uriTeams, resultColumns, null, null, null);
43:         
44:         if (cursor == null) {
45:             System.out.println ("  Internal ERROR: Cursor IS NULL");
46:         }
47:         else if (! cursor.moveToFirst()) {
48:             System.out.println("  No content currently !");
49:         }
50:         else {
51:             
52:             int idColIndex = cursor.getColumnIndex (LeagueDataContract.Teams._ID);
53:             int nameColIndex = cursor.getColumnIndex (LeagueDataContract.Teams.NAME);
54:             int cityColIndex = cursor.getColumnIndex (LeagueDataContract.Teams.CITY);
55:             
56:             LayoutInflater inflater =
57:                     (LayoutInflater) this.getSystemService (Context.LAYOUT_INFLATER_SERVICE);
58:             
59:             do {
60:                 long index = cursor.getLong (idColIndex);
61:                 String name = cursor.getString (nameColIndex);
62:                 String city = cursor.getString (cityColIndex);
63:                 
64:                 LinearLayout row = (LinearLayout) inflater.inflate(R.layout.teams_table_row, null);
65:                 
66:                 TextView viewId = (TextView) row.findViewById(R.id.textViewTeamId);
67:                 viewId.setText(Long.valueOf(index).toString());
68:                 TextView viewTeamName = (TextView) row.findViewById(R.id.textViewTeamName);
69:                 viewTeamName.setText(name);
70:                 TextView viewTeamLocation = (TextView) row.findViewById(R.id.textViewTeamLocation);
71:                 viewTeamLocation.setText(city);
72:                 
73:                 this.table.addView(row);
74:                 
75:             }
76:             while (cursor.moveToNext());
77:         }
78:             
79:         if (cursor != null)
80:             cursor.close();
81:     }
82: }

Beispiel 5. Aktivität „Table of Teams“: Implementierung des Logikfunktionalität in Klasse TeamsActivity.


01: <ScrollView
02:     xmlns:android="http://schemas.android.com/apk/res/android"
03:     android:layout_width="fill_parent"
04:     android:layout_height="fill_parent"
05:     android:scrollbars="vertical"
06:     android:layout_weight="1">
07: 
08:     <TableLayout
09:         android:id="@+id/tableTeamsLayoutAnchor"
10:         android:layout_width="fill_parent"
11:         android:layout_height="wrap_content" >
12:     </TableLayout>
13:     
14: </ScrollView>

Beispiel 6. Aktivität „Table of Teams“: Deklarative Beschreibung der Bedienoberfläche.


01: <?xml version="1.0" encoding="utf-8"?>
02:     
03: <LinearLayout
04:      xmlns:android="http://schemas.android.com/apk/res/android"
05:      android:id="@+id/tableteamsRow"
06:      android:orientation="horizontal"
07:      android:layout_width="match_parent"
08:      android:layout_height="match_parent" >
09: 
10:     <TextView
11:         android:id="@+id/textViewTeamId"
12:         android:layout_width="0dp"
13:         android:layout_weight="10"
14:         android:layout_height="wrap_content"
15:         android:paddingLeft="10dp"
16:         android:paddingTop="2dp"
17:         android:textSize="16sp" />
18:     
19:      <TextView
20:         android:id="@+id/textViewTeamName"
21:         android:layout_width="0dp"
22:         android:layout_weight="50"
23:         android:layout_height="wrap_content"
24:         android:paddingLeft="6dp"
25:         android:paddingTop="2dp"
26:         android:textSize="16sp" />
27:      
28:     <TextView
29:         android:id="@+id/textViewTeamLocation"
30:         android:layout_width="0dp"
31:         android:layout_weight="30"
32:         android:layout_height="wrap_content"
33:         android:paddingLeft="6dp"
34:         android:paddingTop="2dp"
35:         android:textSize="16sp" />
36: </LinearLayout>

Beispiel 7. Aktivität „Table of Teams“: Deklarative Beschreibung einer einzelnen Tabellenzeile.


Implementierung der Aktivität „Table of Games

001: package com.example.leaguedataviewer;
002: 
003: import com.example.leaguedataprovider.LeagueDataContract;
004: 
005: import android.app.Activity;
006: import android.content.ContentResolver;
007: import android.content.Context;
008: import android.database.Cursor;
009: import android.net.Uri;
010: import android.os.Bundle;
011: import android.view.LayoutInflater;
012: import android.widget.LinearLayout;
013: import android.widget.TableLayout;
014: import android.widget.TextView;
015: import java.util.ArrayList;
016: 
017: public class GamesActivity extends Activity {
018:     
019:     private TableLayout table;
020:     private ArrayList<String> namesOfTeams;
021: 
022:     @Override
023:     protected void onCreate(Bundle savedInstanceState) {
024:         super.onCreate(savedInstanceState);
025:         this.setContentView(R.layout.activity_games);
026:         
027:         this.table = (TableLayout) this.findViewById(R.id.tableGamesLayoutAnchor);
028:         
029:         ContentResolver cr = this.getContentResolver();
030:         this.namesOfTeams = MainActivity.retrieveNamesOfTeams(cr);
031:         this.retrieveGames();
032:     }
033:     
034:     // helper methods
035:     private void retrieveGames () {
036: 
037:         ContentResolver cr = this.getContentResolver();
038:         
039:         String resultColumns[] = new String[] {
040:             LeagueDataContract.Games._ID,
041:             LeagueDataContract.Games.MATCHDAY,
042:             LeagueDataContract.Games.TEAMHOME,
043:             LeagueDataContract.Games.TEAMAWAY,
044:             LeagueDataContract.Games.GOALSHOME,
045:             LeagueDataContract.Games.GOALSAWAY
046:         };
047:         
048: 
049:         // retrieve games records
050:         Uri uriGames = Uri.parse(LeagueDataContract.CONTENT_GAMES);
051:         String sortOrder = LeagueDataContract.Games.MATCHDAY;
052:         Cursor cursor = cr.query(uriGames, resultColumns, null, null, sortOrder);
053:         
054:         if (cursor == null) {
055:             System.out.println ("  Internal ERROR: Cursor IS NULL");
056:         }
057:         else if (! cursor.moveToFirst()) {
058:             System.out.println("  No content currently !");
059:         }
060:         else {
061:             
062:             int matchDayColIndex = cursor.getColumnIndex (LeagueDataContract.Games.MATCHDAY);
063:             int teamHomeColIndex = cursor.getColumnIndex (LeagueDataContract.Games.TEAMHOME);
064:             int teamAwayColIndex = cursor.getColumnIndex (LeagueDataContract.Games.TEAMAWAY);
065:             int goalsHomeColIndex = cursor.getColumnIndex (LeagueDataContract.Games.GOALSHOME);
066:             int goalsAwayColIndex = cursor.getColumnIndex (LeagueDataContract.Games.GOALSAWAY);
067:             
068:             LayoutInflater inflater =
069:                 (LayoutInflater) this.getSystemService (Context.LAYOUT_INFLATER_SERVICE);
070: 
071:             do {
072:                 int matchDay = cursor.getInt (matchDayColIndex);
073:                 int teamHome = cursor.getInt (teamHomeColIndex);
074:                 int teamAway = cursor.getInt (teamAwayColIndex);
075:                 int goalsHome = cursor.getInt (goalsHomeColIndex);
076:                 int goalsAway = cursor.getInt (goalsAwayColIndex);  
077:                     
078:                 LinearLayout row = (LinearLayout) inflater.inflate(R.layout.games_table_row, null);
079:                 TextView viewMatchday = (TextView) row.findViewById(R.id.textViewMatchDay);
080:                 viewMatchday.setText(Integer.valueOf(matchDay).toString());
081:                 
082:                 // team id's are 1-based, subtract one for zero-based array list
083:                 String teamHomeName = this.namesOfTeams.get(teamHome - 1);
084:                 String teamAwayName = this.namesOfTeams.get(teamAway - 1);
085:                 TextView view2 = (TextView) row.findViewById(R.id.textViewTeamHome);
086:                 TextView view3 = (TextView) row.findViewById(R.id.textViewTeamAway);
087:                 view2.setText(teamHomeName);
088:                 view3.setText(teamAwayName);
089:                 
090:                 TextView viewResult = (TextView) row.findViewById(R.id.textViewResult);
091:                 viewResult.setText(Integer.valueOf (goalsHome).toString() + " : " +
092:                     Integer.valueOf (goalsAway).toString());
093:                 
094:                 this.table.addView(row);
095:             }
096:             while (cursor.moveToNext());
097:         }
098:             
099:         if (cursor != null)
100:             cursor.close();
101:     }
102: }

Beispiel 8. Aktivität „Table of Games“: Implementierung des Logikfunktionalität in Klasse GamesActivity.


01: <RelativeLayout
02:     xmlns:android="http://schemas.android.com/apk/res/android"
03:     android:layout_width="fill_parent"
04:     android:layout_height="fill_parent"
05:     android:scrollbars="vertical"
06:     android:layout_weight="1" >
07:      
08:     <include
09:         android:id="@+id/tableLayoutHeaderRow"
10:         android:layout_width="fill_parent"
11:         android:layout_height="wrap_content"
12:         layout="@layout/games_table_row"  />
13: 
14:     <View
15:         android:id="@+id/viewSeperator"
16:         android:layout_width="fill_parent"
17:         android:layout_height="1dip"
18:         android:background="#FF000000"
19:         android:layout_marginBottom="10dip"
20:         android:layout_below="@+id/tableLayoutHeaderRow" />
21:         
22:     <ScrollView
23:         android:layout_width="fill_parent"
24:         android:layout_height="fill_parent"
25:         android:layout_below="@+id/viewSeperator"
26:         android:scrollbars="vertical" >
27:     
28:         <TableLayout
29:             android:id="@+id/tableGamesLayoutAnchor"
30:             android:layout_width="fill_parent"
31:             android:layout_height="wrap_content" >
32:         </TableLayout>
33:         
34:     </ScrollView>
35: 
36: </RelativeLayout>

Beispiel 9. Aktivität „Table of Games“: Deklarative Beschreibung der Bedienoberfläche.


01: <?xml version="1.0" encoding="utf-8"?>
02: 
03: <LinearLayout
04:     xmlns:android="http://schemas.android.com/apk/res/android"
05:     android:id="@+id/tableGamesRow"
06:     android:orientation="horizontal"
07:     android:layout_width="match_parent"
08:     android:layout_height="match_parent" >
09: 
10:     <TextView
11:         android:id="@+id/textViewMatchDay"
12:         android:layout_width="0dp"
13:         android:layout_weight="25"
14:         android:layout_height="wrap_content"
15:         android:paddingLeft="10dp"
16:         android:paddingTop="2dp"
17:         android:textSize="16sp"
18:         android:text="@string/textMatchDaySingleLine" />
19:     
20:      <TextView
21:         android:id="@+id/textViewTeamHome"
22:         android:layout_width="0dp"
23:         android:layout_weight="70"
24:         android:layout_height="wrap_content"
25:         android:paddingLeft="6dp"
26:         android:paddingTop="2dp"
27:         android:textSize="16sp"
28:         android:text="@string/textTeamHome" />
29:      
30:     <TextView
31:         android:id="@+id/textViewTeamAway"
32:         android:layout_width="0dp"
33:         android:layout_weight="70"
34:         android:layout_height="wrap_content"
35:         android:paddingLeft="6dp"
36:         android:paddingTop="2dp"
37:         android:textSize="16sp"
38:         android:text="@string/textTeamAway" />
39:     
40:     <TextView
41:         android:id="@+id/textViewResult"  
42:         android:layout_width="0dp"
43:         android:layout_weight="25"
44:         android:layout_height="wrap_content"
45:         android:paddingLeft="6dp"
46:         android:paddingTop="2dp"
47:         android:textSize="16sp"
48:         android:text="@string/textResult" />
49:                
50: </LinearLayout>

Beispiel 10. Aktivität „Table of Games“: Deklarative Beschreibung einer einzelnen Tabellenzeile.


Implementierung der Aktivität „Table of Games (Matchday)

001: package com.example.leaguedataviewer;
002: 
003: import com.example.leaguedataprovider.LeagueDataContract;
004: 
005: import android.app.Activity;
006: import android.content.ContentResolver;
007: import android.content.Context;
008: import android.database.Cursor;
009: import android.net.Uri;
010: import android.os.Bundle;
011: import android.view.LayoutInflater;
012: import android.view.View;
013: import android.widget.AdapterView;
014: import android.widget.AdapterView.OnItemSelectedListener;
015: import android.widget.Spinner;
016: import android.widget.TableLayout;
017: import android.widget.TableRow;
018: import android.widget.TextView;
019: import java.util.ArrayList;
020: 
021: public class GamesOfMatchdayActivity extends Activity implements OnItemSelectedListener {
022:     
023:     // user interface widgets
024:     private TableLayout tableGames;
025:     private Spinner spinnerMatchday;
026:  
027:     // internal data structures
028:     private ArrayList<String> namesOfTeams;
029: 
030:     @Override
031:     protected void onCreate(Bundle savedInstanceState) {
032:         super.onCreate(savedInstanceState);
033:         setContentView(R.layout.activity_games_of_matchday);
034: 
035:         this.tableGames = (TableLayout) this.findViewById(R.id.tableLayoutAnchor);
036:         this.spinnerMatchday = (Spinner) this.findViewById(R.id.spinnerMatchday);
037: 
038:         // retrieve number of teams from content provider
039:         ContentResolver cr = this.getContentResolver();
040:         int numberOfTeams = MainActivity.getNumberOfTeams(cr);
041:         MainActivity.fillSpinnerMatchDays(this, this.spinnerMatchday, numberOfTeams);
042:         this.spinnerMatchday.setOnItemSelectedListener (this);
043:         
044:         // retrieve names of teams from content provider
045:         this.namesOfTeams = MainActivity.retrieveNamesOfTeams(cr);        
046:     }
047:     
048:     // helper methods
049:     private void retrieveGamesOfMatchday (int matchday) {
050:         
051:         // retrieve games records
052:         String resultColumns[] = new String[] {
053:             LeagueDataContract.Games.TEAMHOME,
054:             LeagueDataContract.Games.TEAMAWAY,
055:             LeagueDataContract.Games.GOALSHOME,
056:             LeagueDataContract.Games.GOALSAWAY
057:         };
058:               
059:         Uri uriGames = Uri.parse(LeagueDataContract.CONTENT_GAMES);  
060:         String where = LeagueDataContract.Games.MATCHDAY + " = ?";
061:         String[] selectionArgs = new String[] { String.valueOf(matchday) };
062:         ContentResolver cr = this.getContentResolver();
063:         Cursor cursor = cr.query(uriGames, resultColumns, where, selectionArgs, null);
064:         
065:         if (cursor == null) {
066:             System.out.println ("  Internal ERROR: Cursor IS NULL");
067:         }
068:         else {
069:             
070:             // clear table
071:             this.tableGames.removeAllViews();
072:             
073:             // fill table with new results
074:             if (! cursor.moveToFirst()) {
075:                 System.out.println("  No content currently !");
076:             }
077:             else {
078: 
079:                 int teamHomeColIndex = cursor.getColumnIndex (LeagueDataContract.Games.TEAMHOME);
080:                 int teamAwayColIndex = cursor.getColumnIndex (LeagueDataContract.Games.TEAMAWAY);
081:                 int goalsHomeColIndex = cursor.getColumnIndex (LeagueDataContract.Games.GOALSHOME);
082:                 int goalsAwayColIndex = cursor.getColumnIndex (LeagueDataContract.Games.GOALSAWAY);
083:                 
084:                 LayoutInflater inflater =
085:                     (LayoutInflater) this.getSystemService (Context.LAYOUT_INFLATER_SERVICE);
086:                 
087:                 do {
088:                     int teamHome = cursor.getInt (teamHomeColIndex);
089:                     int teamAway = cursor.getInt (teamAwayColIndex);
090:                     int goalsHome = cursor.getInt (goalsHomeColIndex);
091:                     int goalsAway = cursor.getInt (goalsAwayColIndex);  
092:                     
093:                     TableRow row = (TableRow) inflater.inflate(R.layout.games_of_matchday_table_row, null);
094:                     TextView viewTeamHome = (TextView) row.findViewById(R.id.textViewTeamHome);
095:                     TextView viewTeamAway = (TextView) row.findViewById(R.id.textViewTeamAway);
096:                     TextView viewResult = (TextView) row.findViewById(R.id.textViewResult);
097:                     
098:                     // Id's of teams are 1-based, subtract one for zero-based array list
099:                     String teamHomeName = this.namesOfTeams.get(teamHome - 1);
100:                     String teamAwayName = this.namesOfTeams.get(teamAway - 1);
101:                     viewTeamHome.setText(teamHomeName);
102:                     viewTeamAway.setText(teamAwayName);
103:                     viewResult.setText(Integer.valueOf (goalsHome).toString() + " : " + Integer.valueOf (goalsAway).toString());
104: 
105:                     this.tableGames.addView(row);
106:                 }
107:                 while (cursor.moveToNext());
108:             }
109: 
110:             cursor.close();
111:         }
112:     }
113: 
114:     @Override
115:     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
116:         int selected = this.spinnerMatchday.getSelectedItemPosition();       
117:         this.retrieveGamesOfMatchday (selected + 1);
118:     }
119: 
120:     @Override
121:     public void onNothingSelected(AdapterView<?> parent) {
122:         System.out.println ("GamesOfMatchdayActivity::onNothingSelected");
123:     }
124: }

Beispiel 11. Aktivität „Table of Games (Matchday)“: Implementierung des Logikfunktionalität in Klasse GamesOfMatchdayActivity.


01: <RelativeLayout
02:     xmlns:android="http://schemas.android.com/apk/res/android"
03:     android:layout_width="fill_parent"
04:     android:layout_height="fill_parent"
05:     android:scrollbars="vertical"
06:     android:layout_weight="1">
07:     
08:     <TextView
09:         android:id="@+id/textViewMatchDay"
10:         android:layout_width="fill_parent"
11:         android:layout_height="wrap_content"
12:         android:paddingLeft="6dp"
13:         android:paddingTop="2dp"
14:         android:textSize="18sp"
15:         android:text="@string/textMatchDaySingleLine" />
16:     
17:     <Spinner
18:         android:id="@+id/spinnerMatchday"
19:         android:layout_width="fill_parent"
20:         android:layout_height="wrap_content"
21:         android:paddingLeft="6dp"
22:         android:paddingTop="2dp"
23:         android:layout_marginBottom="40dp"
24:         android:textSize="12sp"
25:         android:layout_below="@+id/textViewMatchDay"
26:         android:drawSelectorOnTop="true" />
27:      
28:     <include
29:         android:id="@+id/tableLayoutHeaderRow"
30:         layout="@layout/games_of_matchday_table_row"
31:         android:layout_below="@+id/spinnerMatchday"
32:         />
33: 
34:     <View
35:         android:id="@+id/viewSeperator"
36:         android:layout_width="fill_parent"
37:         android:layout_height="1dip"
38:         android:background="#FF000000"
39:         android:layout_marginBottom="10dip"
40:         android:layout_below="@+id/tableLayoutHeaderRow" />
41:         
42:     <ScrollView
43:         android:layout_width="fill_parent"
44:         android:layout_height="fill_parent"
45:         android:layout_below="@+id/viewSeperator">
46:     
47:         <TableLayout
48:             android:id="@+id/tableLayoutAnchor"
49:             android:layout_width="fill_parent"
50:             android:layout_height="wrap_content" />
51:         
52:     </ScrollView>
53: </RelativeLayout>

Beispiel 12. Aktivität „Table of Games (Matchday)“: Deklarative Beschreibung der Bedienoberfläche.


01: <?xml version="1.0" encoding="utf-8"?>
02: 
03: <TableRow 
04:     xmlns:android="http://schemas.android.com/apk/res/android"
05:     android:id="@+id/tableLayoutHeaderRow"
06:     android:layout_width="fill_parent"
07:     android:layout_height="wrap_content"
08:     android:paddingLeft="6dp"
09:     android:paddingTop="2dp"
10:     android:textSize="12sp"        
11:     android:layout_below="@+id/spinnerMatchday">
12:              
13:     <TextView
14:         android:id="@+id/textViewTeamHome"
15:         android:paddingLeft="6dp"
16:         android:paddingTop="2dp"
17:         android:layout_width="0dp"
18:         android:layout_weight="80"
19:         android:layout_height="wrap_content"
20:         android:text="@string/textTeamHome" />
21:      
22:     <TextView
23:         android:id="@+id/textViewTeamAway"
24:         android:paddingLeft="6dp"
25:         android:paddingTop="2dp"
26:         android:layout_width="0dp"
27:         android:layout_weight="80"
28:         android:layout_height="wrap_content"
29:         android:text="@string/textTeamAway" />
30:     
31:     <TextView
32:         android:id="@+id/textViewResult"
33:         android:paddingLeft="6dp"
34:         android:paddingTop="2dp"
35:         android:layout_width="0dp"
36:         android:layout_weight="30"
37:         android:layout_height="wrap_content"
38:         android:text="@string/textResult" />
39:                
40: </TableRow>

Beispiel 13. Aktivität „Table of Games (Matchday)“: Deklarative Beschreibung einer einzelnen Tabellenzeile.