2012-05-21

Problem med View Swiping och orientation changes


Jag har byggt en app med View Swiping enligt den här artikeln. Jag har gjort några ändringar för att det ska passa min app.

Min implementation:
En FragmentActivity som har en ViewPager.
ViewPager har en FragmentPagerAdapter.
I FragmentPagerAdapter så skapas det upp 6 stycken ListFragment.
Varje ListFragment har en ArrayAdapter som ska hålla sina Items.
Jag lägger till Items till varje adapter beroende på vilken status ett item har med:
fragmentPagerAdapter.getItem(listIndex).getAdapter ().add(item);
Det fungerar galant första gången jag visar mina listfragment.

Problemet:
När jag vrider på telefonen så skapas allt om via onDestroy() och onCreate() som det ska, men mina listfragment blir tomma. Jag har skapat om dem så det borde bli samma sak som första gången jag laddar allt.
Felet beror på att Android återanvänder listfragmenten som skapades första gången och de är inte samma som jag lägger till mina items till. Därför blir listorna tomma.
Jag använder getItem() för att få tag i mitt listfragment, men det gör inte Android. Den anropar inte ens getItem() andra gången utan letar reda på dem via instantiateItem().

Min lösning:
Lösningen känns lite som ett hack, men fungerar väldigt bra (det skapas dock upp några extra fragment, men det är nog inte jätteofta som användarna kommer att vrida telefonen fram och tillbaks just i listvyn där det här används):
Gör en override på: public Object instantiateItem(ViewGroup container, int position) i FragmentPagerAdapter:
Kod:
@Override
public Object instantiateItem(ViewGroup container, int position) {
ItemsFragment fragment = (ItemsFragment) super.instantiateItem(container, position);
if(fragment.getAdapter() == null) {
ItemsFragment myFragment = getItem(position);
fragment.setListAdapter(myFragment.getAdapter());
fragment.setTitle(myFragment.getTitle());
}
return fragment;
}
Notera: Mitt ItemsFragment har en ny metod getAdapter() som returnerar en typad adapter "ItemListAdapter" och nya metoder för set och getTitle() som används för att få rätt texter på PagerTitleStrip i ViewPagern.

Jag använder min metod getAdapter() för att kolla om det funna fragmentet har en adapter. Om den inte har det så tar jag en adapter via metoden getItem() som returnerar fragments som jag har kontroll på.

Ni kanske undrar varför jag helt enkelt inte bara lägger till:
android:configChanges= "orientation|keyboardHidden|screenSize"
i manifestet. För det första så ska den tekniken undvikas eftersom det gör hanteringen av resources onödigt komplicerad. Vidare så hanteras inte ActionBar och PagerTitleStrip korrekt.

Nästa steg nu är att använda:
onRetainCustomNonConfigurationInstance/getLastCustomNonConfigurationInstance
för att slippa ladda om all data mellan vridningarna av telefonen.

Ref: Mitt inlägg på Swedroid

2012-05-13

Automatiserad testning av Android-appar

Jag har den här veckan lagt in Robotium som testverktyg för den Android-app, MantisDroid, som jag håller på och utvecklar. Med hjälp av Robotium så kan jag sätta upp helt automatiserade tester av hela appen som kan köras både på emulator och riktig device. Jag har även byggt in stöd för att samtidigt testa mot flera olika versioner av MantisBT (en buggtracker som hanterar defekter för projekt) som är den servertjänst min app går mot.


Robotium är ett testverktyg som kör tester mot användargränssnittet på en app som kör på antingen en emulator eller riktig device. Den simulerar klick på knappar och andra GUI-kontroller, text-hantering i textboxar, den verifierar att olika kontrollera och texter hittas m.m. Kolla på videon ovan för att få en snabb introduktion i hur det fungerar.

Jag har byggt upp mina tester i en klass för varje typ av test, dvs. en klass som hanterar Login-hantering, en för Project och en för Issues.För att hantera flera olika versioner av MantisBT har jag skapat en Enum (MantisBTUrlEnum) som håller i URL:erna för de olika versionerna. Varje testfall loopar över alla värden i Enum:en och plockar ut den URL som ska testas och kör testet mot den. Det är alltså viktigt att återställa miljön efter varje körning så att nästa loop har samma förutsättningar som varvet innan. Om jag varit testpuritan så skulle jag kanske skapat ett testfall för varje version, men jag tycker att även bland testfallen så ska duplicering av kod undvikas. En idé som slog mig nu var att bryta ut själva testet i en egen metod och sedan låta varje testfall anropa den metoden med den URL som ska testas. Nu kommer jag dock att behålla min implementation för den fungerar bra och för tillfället har jag bara en enda URL att testa mot. När det blir fler URL:er så kanske jag bryter isär testfallen om det är fördelaktigt på något sätt.

Tips:

Använd inte hårdkodade strängar. Om ni gör det så kommer testerna att fallera om en sträng ändras eller om ni lägger till fler språk. Använd istället: solo.getString(id);
Ex.
För att hämta strängen "Login" så skriver ni:
solo.getString(se.nextsource.android.mantisdroid.R.string.login);
För att hämta "OK" som ligger inbyggt i Android så skriver ni:
solo.getString(android.R.string.ok);

Jag har stött på följande problem:

Problem - returnerar fel aktivitet vid uppstart:
I setUp() vill jag kontrollera att jag är på rätt aktivitet, men solo.getCurrentActivity() ger fel aktivitet som svar. Den returnerar den aktivitet som angetts vid uppstart. I min applikation anger jag LoginActivity som startaktivitet, men om användaren redan är inloggad så flyttas den automatiskt till MainActivity. Robotium har inte koll på det.
Lösning:
Lägg en sökning efter en textsträng som finns på den aktivitet du önskar starta med i en while-loop. I mitt fall är det en knapp med texten "Login". Loopa tills den strängen hittas. Inuti while-loopen så kontrollerar jag om jag är på MainActivity genom att söka efter en viss textsträng där och om den hittas så tar jag upp menyn och klickar på "Logout":
solo.clickOnMenuItem("Logout"));
I alla andra fall så trycker jag på back-knappen:
solo.goBack();

Problem - hittar inte knappar när soft keyboard är synligt:
När man klickar på en EditTextPreference i en PreferenceActivity så får man på en verklig device upp soft keyboard eftersom textfältet får fokus automatiskt. När soft keyboard är uppe så går det inte att klicka på "OK"-knappen. Knappen hittas med solo.searchButton("OK"); men är inte klickbar. På emulatorn inträffar inte det här eftersom soft keyboard inte kommer fram. Det saknas metoder i Robotium för att kontrollera om soft keyboard är framme eller inte.
Lösning:
Skapa en metod som stänger soft keyboard och använd den då soft keyboard automatiskt poppar upp:

@SuppressWarnings("static-access")
private void closeSoftKeyboard() {
InputMethodManager imm = (InputMethodManager)getActivity().
getSystemService(getActivity().INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(solo.getEditText(0).getWindowToken(), 0);
}

Problem - kan inte klicka på knappar i ActionBar:
Robotium saknar stöd för att klicka på knappar i ActionBar.
Lösning:
Använd Instrumentation och id:et för knappen för att klicka på den:
getInstrumentation().invokeMenuActionSync(solo.getCurrentActivity(), se.nextsource.android. mantisdroid.R.id.menu_settings, 0);

2012-05-02

NFC-taggar


Igår kom jag på att jag ville använda NFC för att slå av och på framför allt Wi-Fi och blåtand. Sedan dess har jag hållt på och kollat på olika taggar och olika appar för att hantera det. Jag fastnade för Xperia SmartTags, men fick inte appen med Widgeten att funka i min Nexus S. Troligtvis köper jag nog SmartTags ändå eftersom man får 4 snygga taggar för 120 kr + frakt (hos Dustin). Droidstuffs taggar var inte så snygga...tyvärr. Skaffa snyggare taggar så köper jag dem därifrån direkt! :-)
Idag fick jag tips om pluginen Locale NFC Plugin som fungerar ihop med Tasker som jag köpt för länge sedan. Nu kan jag använda RFID-taggen i mitt Västtrafikkort till att slå av Wi-Fi och blåtand, sätta ned ljusstyrkan och höja ringsignalen till max.

Fördelen med Locale NFC Plugin är att den funkar med alla taggar, inkl. de icke skrivbara.

Om någon undrar så gjorde jag så här:
1. Öppna Tasker och gå till fliken Tasks
2. Skapa nytt Task och döp till lämpligt namn "NFC Out"
3. Lägg till de Actions du önskar göra när Tasken körs: Wi-Fi off, BT off, Ringer Volume 7 etc.
4. Spara ditt Task
5. Gå till fliken Profiles
6. Skapa ny profil och döp till lämpligt namn "NFC Out"
7. Välj Context: State->Plugin->Locale NFC Plugin
8. Tryck Edit och håll din telefon mot taggen, tryck Ok
9. Spara och välj den Task du tidigare skapade (NFC Out)
10. Testa! Fungerar grymt bra!