Ciekawe nieoczywiste sprawy: 56. Widokowi TextView treść, którą ma wyświetlać, ustawiamy atrybutem text. Dziwne, ale tego atrybutu nie da się ustawić w stylu (więc i w motywie). Mimo, że w c:\Users\\AppData\Local\Android\android-sdk\platforms\android-7\data\res\values\attrs.xml widzę, że ten atrybut jest w sekcji declare-styleable. 57. Jeśli chcemy, żeby tekst wyświetlany przez TextView miał różne fragmenty sformatowane różnie, da się, choć nie testowałem. Jeden sposób to stworzyć obiekt klasy Spannable i wołać na nim metodę setSpan - ta metoda ustawia formatowanie fragmentowi tekstu. Drugi sporób to zawołać metodę TextView#setText i przekazać nie String, ale obiekt stworzony przez metodę Html.fromHtml (ta metoda tworzy Spanned na podstawie Stringa zawierającego tekst oznakowany HTML-em). 58. W Androidzie nie ma dziedziczenia layoutów. Można tylko odwrotnie: w jednym laoucie inkludować inny. 59. W CSS-ie jest tak, że niektórych atrybutów domyślną wartością jest "inherit". Znaczy, że jak tego atrybutu jakiemuś elementowi nie ustawię, to że będzie miał domyślnie ustawione tak, jak ma ustawione rodzic. Zastanawiałem się, czy coś podobnego jest w androidowych layoutach. Nie ma. 60. Istnieją aplikacje, których twórcy nie przewidzieli, że kiedyś będą pokazywane na dużym ekranie - przez co na dużym ekranie rozkładają się brzydko. Dlatego aplikacje mogą być uruchamiane w trybie kompatybilnym (screen compatibility mode). Są dwa screen compatibility mode. Starszy, praktycznie już nie stosowany, polegał na tym, że aplikacja była rysowana na środku ekranu na niedużym prostokącie (mniej więcej takiej wielkości, jakiej wielkości jest mały ekran telefonu), a dookoła była czarna ramka. Jeżeli nasza aplikacja minimalne API ma ustawione na 4 lub więcej, ten tryb na pewno nie będzie zastosowany. I drugi tryb, który polega na tym, że aplikacja jest rysowana na niewielkim prostokącie, a ten prostokąt jest potem rozciągany na pełen ekran. Aplikacja jest (podobno, nie sprawdzałem praktycznie) uruchamiana w trybie kompatybilnym, jeśli jest uruchomiona na dużym ekranie, a w manifeście nie napiszemy, że obsługujemy duży ekran, lub napiszemy, że nie obsługujemy dużego ekranu. Kiedy aplikacja jest uruchomiona w trybie kompatybilnym, użytkownik ma możliwość wyłączenia trybu kompatybilnego. 61. Kiedy robię własny widok dziedziczący po View, to on w onDraw dostaje canvas, który za każdym razem jest czysty - więc nie muszę dbać o to, żeby go wyczyścić ze starych rzeczy, zanim narysuję nowe. A kiedy dziedziczę po SurfaceView, to za każdym razem jak z lockCanvas dostaję canvas, to jest już na nim to, co rysowałem poprzednio, więc pewnie chcę za każdym razem wyczyścić, co było poprzednio. 62. Czasem jak importuję do eklipsa jakiś projekt androidowy, to pojawia się taki błąd: Android requires compiler compliance level 5.0 or 6.0. Found '1.7' instead. Please use Android Tools > Fix Project Properties. Wbrew temu, co mówią, nie trzeba robić Android Tools > Fix Project Properties. Trzeba wejść we właściwości projektu (alt + enter), tam wejść w "java compiler" i compiler compliance level zmienić z 1.5 na 1.6. 63. Zwykły, normalny i zalecany przez nich (przynajmniej dziś, w wielką sobotę 2013) sposób instalacji narzędzi polega na tym, żeby ściągnąć od nich ze strony rzecz o nazwie adt-bundle, rozpakować, stamtąd odpalić eclipse\eclipse.exe. I to tyle. Ten bundle zawiera wszystko, co potrzebne. 64. W OpenGL ES 1 obie funkcje - glDrawArrays i glDrawElements - mogą być zaimplementowane tak, że jak punkt jest użyty w kilku trójkątach, to karta graficzna nie będzie przetwarzać go kilka razy (osobno przy każdym wyświetleniu w jakimś trójkącie), ale tylko raz. Ta optymalizacja może być ograniczona tylko do kilkukrotnego wykorzystania w jednym wywołaniu funkcji glDrawArrays lub glDrawElements. Źródło: czerwona książka, http://www.glprogramming.com/red/chapter02.html : "With both glArrayElement() and glDrawElements(), it is also possible that your OpenGL implementation caches recently processed vertices, allowing your application to "share" or "reuse" vertices." 65. Zastanawiałem się, co to jest za klasa BuildConfig, co ją Eclipse automatycznie generuje. A jest to klasa zawierająca informacje o tym, jak jest budowana aplikacja - na produkcję czy do testów. Można na przykład zrobić, żeby w zależności od tej informacji do logów pisać więcej lub mniej. 66. Myślałbym, że jak się (korzystając z OpenGL-a) będzie robić pushMatrix, a zapomni robić w parze popMatrix, to się coś przepełni i program się wywali - ale u mnie tak się nie dzieje. 67. Zauważyłem, że jak spróbuję użyć gl.glEnable(GL10.GL_DEPTH_TEST) do zrobienia, żeby bliższe rzeczy przysłaniały dalsze rzeczy, to nieraz to nie działa dobrze, jeśli nie korzystam z gluPerspective. Może to ma jakiś związek z ustawieniem odległości najbliższych i najdalszych widocznych obiektów, którą to odległość się ustawia w gluPerspective? Podobne depthbuffer zawiera głębokość znormalizowaną tak, że 0 to odległość najbliższych widocznych obiektów, a 1 to odległość najdalszych, więc - ostrzegają ludzie - jeśli te odległości ma się ustawione na bardzo różne, to przedmioty będące dość blisko siebie po takim znormalizowaniu mogą mieć głębokość tak niewiele różniącą się od siebie, że przy zaokrągleniu wyjdzie, że mają tę samą odległość. Ale w sumie to nie powinno być problemem, bo podobno jak nie ustawię inaczej, to widzę odległość od 0 do 1. No to nie wiem. Ale lepiej nie bawić się z tym bez włączania perspektywy. 68. Pisało gdzieś, że jak ustawiam odległość najbliższych i najdalszych widocznych obiektów, to trzeba pamiętać, że ona nie może być ujemna ani zerowa, bo będą dziwne problemy. Trochę mi się kłóci z tym, że podobno jak nie ustawię inaczej, to widzę głębię od 0 do 1, ale może lepiej jednak tego przestrzegać. 69. Zastanawiałem się kiedyś, jak działa w OpenGL-u zasłanianie (to robione przez gl.glEnable(GL10.GL_DEPTH_TEST)). Czy trójkąty zasłaniają trójkąty, czy piksele zasłaniają piksele? Na przykład jeśli bliższy trójkąt zasłoni tylko kawałek dalszego trójkąta, to czy tego dalszego będzie widać kawałek czy cały? Albo jeśli bliższy trójkąt niby całkiem zasłania dalszy, ale jest narysowany jako obramowanie (sposobem GL10.GL_LINE_LOOP), to czy tamten dalszy trójkąt będzie widoczny? Odpowiedzi brzmią: piksele zasłaniają piksele (a dokładniej: fragmenty zasłaniają fragmenty, ale fragmenty to prawie jak piksele, tylko przed czymś tam - przed antyaliasowaniem?), więc będzie widać kawałek, a dalszy będzie niewidoczny. 70. W OpenGL układ współrzędnych jest taki: oś Y do góry, oś X w prawo, oś Z do mnie. 71. W OpenGL stos macierzy (ten, na który odkładamy przez glPushMatrix) ma ograniczoną wielkość. Jego wielkość możemy sprawdzić - tak jak różne inne parametry - metodą glGetIntegerv. Kiedy go przepełnimy, glPushMatrix się nie powiedzie. A o tym, że był jakiś błąd, możemy się przekonać - tak jak z każdym błędem - wołając glGetError. Oto przykład: public class NajprostszyRenderer implements Renderer { public void onSurfaceCreated(GL10 gl, EGLConfig arg1) { int[] wynik = new int[1]; gl.glGetIntegerv(GL10.GL_MAX_MODELVIEW_STACK_DEPTH, wynik, 0); Log.i("test", "wynik=" + wynik[0]); gl.glMatrixMode(GL10.GL_MODELVIEW); Log.i("test", "blad=" + gl.glGetError()); for (int i=0; i<=wynik[0]+3; i++) { gl.glPushMatrix(); Log.i("test", "blad=" + gl.glGetError()); } } public void onSurfaceChanged(GL10 gl, int width, int height) { } public void onDrawFrame(GL10 gl) { } } 72. Kiedy korzystając z OpenGL obracamy metodą glRotatef, o tym, czy obracamy zgodnie z ruchem wskazówek zegara czy przeciwnie, mówi znak kąta i zwrot wektora. Kiedy kciuk prawej ręki skieruję w stronę, w którą wskazuje ten wektor i lekko ugnę palec wskazujący, to pokaże on, w którą stronę będzie obracać się figura. 73. Częścią SDK do Androida jest narzędzie do debuggowania OpenGL-a - Tracer for OpenGL. Ale wymaga, żeby urządzenie miał Androida w wersji API przynajmniej 16. Ale chodzi na emulatorze. Ale nie bardzo umiem go używać. 74. Zastanawiałem się ostatnio, jak to się dzieje, że na górze ekranu jest pasek z ikonkami. Wydawało mi się, że widok rysujący ten pasek jest częścią hierarchii widoków, którą mogę podglądać hierarchyviewerem. Ale właśnie sprawdziliśmy: nie jest. Korzeniem hierarchii widoków mojego procesu jest widok, który zajmuje całe okno oprócz tego paska. Więc to, wydaje się, działa tak. Jest w systemie operacyjnym jakiś zarządca ekranu, od którego można - wołając jakieś wywołanie systemowe - dostać prostokąt ekranu. I moja aplikacja dostaje ten prostokąt, rządzi nim obiekt klasy window, on jednemu widokowi pozwala tam rysować, ten widok jest lejałtem więc swój obszar dzieli na mniejsze prostokąty i daje je swoim dzieciom - i tak dalej. Mój proces, wydaje się, nie może zrobić zrzutu ekranu z tej części, gdzie jest ten pasek, ani nie może tam rysować - chyba że poprosi o fullscreen. 75. Jak się dłużej dotknie coś, na czym jest zarejestrowane menu kontekstowe, a potem wybierze pozycję z menu, zostaje wywołana metoda onContextItemSelected, przekazany jej zostaje obiekt item. Można na nim wywołać getMenuInfo, a dostanie się obiekt z dodatkowymi informacjami. Nie wiadomo z góry (w czasie kompilacji), jakie będą te dodatkowe informacje, bo menu kontekstowe na różnych widokach daje różne dodatkowe informacje (na przykład menu kontekstowe na liście daje w tym obiekcie informację, na którym elemencie listy kliknęliśmy). Więc metoda getMenuInfo mogłaby mieć zadeklarowane, że zwraca Object, a my, wiedząc, na czym jest nasze menu kontekstowe, byśmy eksplicytnie rzutowali na odpowiedni typ. Ale to byłoby tak bezczelnie brzydkie, że twórcy Androida to ukryli. Metoda getMenuInfo ma zadeklarowane, że zwraca rzecz interfejsu ContextMenu.ContextMenuInfo, ale ten interfejs nie ma żadnych metod (więc jest tylko marker interface). I są w ogóle niepodobne do siebie klasy implementujące ten interfejs, a my eksplicytnie rzutujemy na odpowiedni z nich. Ciekawe, że przy metodzie getSystemService tak się nie szczypali - getSystemService zwraca po prostu Object. 76. Jak rysowałem na ekranie czachę z Montezumy, ale dużą, to ona brzydko wyglądała - bo przez interpolację miała rozmyte brzegi. Szukałem, jak wyłączyć drawablowi interpolowanie, ale wydaje się, że to nie tak prosto. Ale jest prostszy sposób na takie potrzeby - wystarczy przygotować sobie w Gimpie powiększoną wersję obrazka z czachą, ale przeskalowaną metodą najbliższego sąsiada. I ten obrazek rysować, jeśli potrzeba skalując w dół. 77. If more than one object should be stored across activities and configuration changes, you can implement an Application class for your Android application. 78. Błędem czasu wykonania byłoby dwukrotne w jednym wątku zawołanie Looper.prepare – spowodowałoby to rzucenie wyjątku dziedziczącego po RuntimeException. 79. Zastanawiałem się, co będzie, jak komunikat pobrany z jednego handlera przekażę do wykonania drugiemu. Objawy są takie, że wykonuje się tym handlerem, któremu został przekazany przez sendMessage, niezależnie od tego, którym był stworzony. To dlatego że - właśnie obejrzałem źródło - Looper#loop w nieskończonej pętli bierze z kolejki komunikaty i na każdym robi msg.target.dispatchMessage(msg). Więc decydujące jest, do którego handlera jest referencja w atrybucie target komunikatu. A ta referencja jest ustawiana dwa razy: najpierw jest ustawiana przy pobieraniu komunikatu przez Handler#obtain, a potem znowu jest ustawiana przy wysyłaniu przez sendMessage. Więc referencja z obtain nie ma znaczenia, bo i tak jest przykrywana przy sendMessage. Dziwne to - to po co Handler#obtain umieszcza w komunikacie referencję do handlera, przez którego był stworzony? 80. Komunikat można dać handlerowi na trzy sposoby: wykonaj jak najszybciej, wykonaj za określony czas, wykonaj o określonej godzinie. Dwie pierwsze są cienkim opakowaniem na wykonaj o określonej godzinie. 81. Kiedy mam aplikację taką jak gra w paletkę - że jest jeden widok, napisany przez nas, który rysuje grę - i chcemy, żeby aktywność przy obrocie nie była restartowana, jest to proste. Wystarczy w manifeście, w atrybutach aktywności dopisać atrybut: android:configChanges="keyboardHidden|orientation|screenSize" (to screenSize jest potrzebne w nowszych wersjach Androida). I już. Po obróceniu telefonu Android nie zrestartuje aktywności, a ponieważ rysowanie zawsze jest robione w układzie współrzędnych, którego początek jest w aktualnym lewym górnym rogu ekranu, gra po prostu sama się obróci. To wszystko. Bardzo proste i działa. Ale tego sposob twórcy Androida nie zalecają, bo on jest wbrew duchowi Androida - że aplikacja ma być gotowa na to, że aktywności mogą być zabijane i tworzone zawsze, kiedy będzie to potrzebne. 82. Kursant spytał: W praktyce aplikacje będą składały się z wielu aktywności. Często będzie otwartych wiele aktywności. Jak to jest z tym obróceniem - reset aktywności będzie dotyczył wszystkich jednocześnie, czy tylko top-level, a po powrocie do poprzedniej także tej poprzedniej itd. Odpisałem: Dokumentacja mówi tylko: http://developer.android.com/guide/topics/resources/runtime-changes.html "When such a change occurs, Android restarts the running Activity" Dokumentacja nie mówi, czy i kiedy są restartowane aktywności, które nie są running. Ale zdrowy rozum sugeruje, że byłoby bardzo dziwne zarówno gdyby nie były restartowane wcale, jak też gdyby były restartowane od razu (przecież za chwilę konfiguracja telefonu może wrócić do poprzedniej i restart okazałby się niepotrzebny). Dlatego jedyne roządne zachowanie to to, że restartowana jest tylko bieżąca aktywność, a kiedy wracamy do wcześniejszej aktywności, jest sprawdzane, czy były one uruchomione na konfiguracji takiej, jaka jest teraz, a jeśli na innej, są restartowane. Próby potwierdzają, że tak zachowuje się aktualna wersja Androida. 83. Kursant spytał: Czy (przy użyciu jakichś bibliotek zgodności) da się pisać aplikacje tak, aby efektywnie wykorzystywały ficzery wersji > 3.0 (typu fragmenty) i działały zarazem na wersji 2.1 i wyższej ? Czy te biblioteki zastąpią odwołania do nowoczesnych ficzerów, tym co będzie na danym urządzeniu możliwe? Odpowiedziałem: Tak, przy użyciu bibliotek zgodności można pisać aplikacje na niskie wersje Androida korzystając z ficzerów dostępnych tylko w wersjach wyższych. Biblioteki zgodności nie dają wszystkich ficzerów z wersji wyższych, a tylko niektóre. Jakie, to jest wymienione w dokumentacji: http://developer.android.com/reference/android/support/v4/app/package-summary.html http://developer.android.com/reference/android/support/v13/app/package-summary.html W Internecie ludzie piszą, że kiedy uruchamiamy aplikację korzystającą z bibliotek zgodności na urządzeniu o wersji API tak wysokiej, że na nim którym dany ficzer jest dostępny normalnie, biblioteka zgodności po sprawdzeniu numeru wersji wywołuje daną rzecz z normalnych bibliotek androidowych - ale to nie zawsze jest prawda. Klasa android.support.v4.view.MenuCompat, na przykład, robi tak - proszę spojrzeć na jej źródła: http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/support/v4/view/MenuCompat.java#51 Select the correct implementation to use for the current platform. 55 56 static final MenuVersionImpl IMPL; 57 static { 58 if (android.os.Build.VERSION.SDK_INT >= 11) { 59 IMPL = new HoneycombMenuVersionImpl(); 60 } else { 61 IMPL = new BaseMenuVersionImpl(); 62 } 63 } 64 Ale już klasa android.support.v4.app.Fragment tak nie robi, co mówi sama dokumentacja: http://developer.android.com/reference/android/support/v4/app/Fragment.html "When running on Android 3.0 or above, this implementation is still used; it does not try to switch to the framework's implementation." 84. Kursant pytał, jak działa findViewById. Sprawdziłem, tak: Każdy widok ma atrybut mID, typu int. W nim jest trzymane id tego widoku ustawione w pliku z layoutem (lub ustawione setterem): http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.0_r1/android/view/View.java#1874 case com.android.internal.R.styleable.View_id: mID = a.getResourceId(attr, NO_ID); break; Metoda findViewById korzysta z metody findViewTraversal, która w różnych widokach działa różnie. Widoki zwykłe (nie będące ViewGroup) mają wersję odziedziczoną po View - zwraca ona ten obiekt (jeśli szukane id jest zgodne z id tego widoku) lub nulla: http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.0_r1/android/view/View.java#7528 7528 protected View findViewTraversal(int id) { 7529 if (id == mID) { 7530 return this; 7531 } 7532 return null; 7533 } Klasa ViewGroup przykrywa tę metodę na taką, która najpierw sprawdza swoje id, a jeśli to się nie zgadza, woła findViewById na swoich dzieciach: http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/1.5_r4/android/view/ViewGroup.java#1588 1588 @Override 1589 protected View findViewTraversal(int id) { 1590 if (id == mID) { 1591 return this; 1592 } 1593 1594 final View[] where = mChildren; 1595 final int len = mChildrenCount; 1596 1597 for (int i = 0; i < len; i++) { 1598 View v = where[i]; 1599 1600 if ((v.mPrivateFlags & IS_ROOT_NAMESPACE) == 0) { 1601 v = v.findViewById(id); 1602 1603 if (v != null) { 1604 return v; 1605 } 1606 } 1607 } 1608 1609 return null; 1610 } 84. Łukasz Kukawski zapytał mnie raz: Ze szkolenia pamiętam mówił Pan aby zadbać o to aby każde połączenie z bazą danych było nawiązywane z oddzielnym wątku. Czy korzystając z ContentProvaider który pobiera nam dane, tez musimy o to zadbać, czy może ContentProvider sam otwiera się w nowym wątku? Jeżeli nie, to w jaki sposób to zrobić łącząc AsyncTask-a, w metodzie query ContentProvaidera dodać AsyncTaska? Jak to jest zazwyczaj rozwiązywane? Na co ja odpowiedziałem: Tak, rzeczywiście mówiłem, że zapytania lepiej wykonywać w osobnym wątku. Jeśli tej zasady nie przestrzegać, może nie być dużego problemu - zwykle wykonanie zwykłego zapytania nie trwa na tyle długo, żeby zablokować aplikację na zauważalny dla użytkownika czas. Jeśli jednak jest to pierwsze wykonanie zapytania po zainstalowaniu albo zaktualizowaniu aplikacji, przed wykonaniem zapytania helper może musieć coś zmienić w bazie danych - dodać nowe tabelki lub wypełnić je danymi. A to - na przykład jeśli danych do wypełnienia tabelek jest dużo - może chwilę potrwać. Jeśli zdecydujemy się przestrzegać tej zasady, z content providerem jest tak: Nie, content provider sam z siebie nie wykonuje metody query w osobnym wątku. Zresztą nie mógłby wykonywać jej w osobnym wątku, bo musi zwrócić kursor, więc nie może tego działania wykonać asynchronicznie. Gdyby metoda query uruchamiała osobny wątek (wykonujący zapytanie), a sama kończyła działanie (żeby główny wątek, w którym została uruchomiona, mógł w czasie wykonywania zapytania robić inne rzeczy), to metoda query kończyłaby działanie, kiedy zapytanie jeszcze się nie zakończyło, więc nie mogłaby zwrócić kursora. Gdyby twórcy Androida zdecydowali się zrobić content providera tak, że metoda query byłaby wykonywana w osobnym wątku, ta metoda nie mogłaby zwracać kursora, tylko pewnie przyjmowałaby jako argument callbacka, który byłby uruchamiany kiedy zapytanie się skończy, i w którym to callbacku mówilibyśmy, co ma być zrobione z otrzymanymi danymi. A dokładniej, w pewnej sytuacji pojawia się osobny wątek, ale niewiele nam to pomaga. Jeśli content provider, z którego pobieramy dane, pochodzi z innej aplikacji, to kiedy ja w mojej aplikacji zawołam metodę query na content resolverze (otrzymanym pewnie z Context#getContentResolver), to metoda query content resolvera nie może oczywiście tak po prostu zawołać metody query na content providerze z innej aplikacji, bo każda aplikacja działa w osobnym procesie, w osobnej wirtualnej maszynie Javy. W tej sytuacji metoda query mojego content resolvera prosi międzyprocesowo proces tamtej drugiej aplikacji o wykonanie w swoim procesie metody query i zwrócenie otrzymanego kursora, a następnie zamiera i czeka (blokując wątek, w którym na content resolverze zawołałem query), aż dostanie odpowiedź z tego żądania międzyprocesowego. Do czasu otrzymania odpowiedzi wątek, w którym na content resolverze zawołałem query, musi być zablokowany - bo jedyny sposób na to, żeby nie był zablokowany, to byłoby wyjście z metody query, ale z niej, jak mówiłem, nie możemy wyjść, dopóki nie mamy kursora, bo ta metoda musi zwrócić ten kursor. Ale druga aplikacja - ta, w której jest content provider, którego prosimy o kursor - żądania międzyprocesowe obsługuje nie w swoim głównym wątku (tym zarządzającym GUI), ale w innym, osobnym. Więc kiedy z głównego wątku wołam query prosząc o dane content providera z mojej aplikacji, blokuję sobie główny wątek. A kiedy z głównego wątku wołam query prosząc o dane content providera z innej aplikacji, blokuję sobie główny wątek, ale nie blokuję głównego wątku w cudzej aplikacji. Co jest zresztą sprawiedliwe - mogę zablokować główny wątek swojej aplikacji, ale nie mogę zablokować głównego wątku cudzej aplikacji. Jeśli chcę, żeby przy wołaniu query na content providerze zapytanie do bazy danych odbywało się w osobnym wątku, muszę albo użyć loadermanagera (istnieje od API 11, http://developer.android.com/reference/android/app/LoaderManager.html ), albo zadbać o to sam. Jeśli chcę zadbać o to sam, kodu odpalającego osobny wątek nie mogę napisać w metodzie query content providera (bo ta metoda musi zwrócić kursor będący wynikiem zapytania, więc i tak nie mogłaby zakończyć działania, dopóki ten osobny wątek nie dałby kursora, więc i tak blokowałaby wątek, w którym została uruchomiona). Muszę więc osobny wątek uruchamiać tam, gdzie korzystam z content providera - to znaczy uruchomić (na przykład korzystając z asynctaska) osobny wątek i w nim wywołać query na content resolverze. Mogę też nie przejmować się tym wszystkim i wołać query z głównego wątku - jeśli wiem, że w mojej aplikacji ani tworzenie bazy danych przy pierwszym uruchomieniu, ani jej aktualizacja przy aktualizacji aplikacji do nowej wersji nie trwa długo, nic złego się nie stanie. 85. Jak w SDK manadżerze chcę zainstalować obraz (do emulatora) do nowych wersji API, muszę wybrać "intel x86 atom system image". Ale przy starszych wersjach API w SDK managerze nie ma "system image" do zainstalowania. Więc jak zainstalować sobie system image do starszych wersji API, żeby móc sobie uruchamiać w emulatorze starsze wersję Androida? Bo one chodzą szybciej pod emulatorem. Rozwiązanie: dla wersji API przed 14 trzeba zainstalować "SDK platform" - to instaluje zarówno wydmuszki bibliotek, żebyśmy mogli kompilować, jak i obraz systemu, żeby go uruchamiać pod emulatorem. I potem zrestatować Eclipse. Źródło: http://stackoverflow.com/questions/13461568/install-old-system-image-with-android-tools-r21 86. Fragment wstawiany w layout musi mieć nadane id lub taga. Nawet jeśli mi nie zależy, żeby jego stan się zachowywał (bo na przykład robię prosty test), musi mieć. 87. Kiedyś nie pamiętałem, co to jest tag widoku. Każdy widok ma tag, będący referencją na dowolny obiekt. Można go odczytać i ustawić w kodzie w Javie (metodami getTag i setTag), można w kodzie w Javie wyszukać widoki o określonym tagu (metodą findViewWithTag), można go ustawić w pliku XML z layoutem (atrybutem android:tag). Można też widokowi ustawić kilka tagów o różnych id, bo metody getTag i setTag mają przeciążone wersje przyjmujące oprócz normalnych argumentów jeszcze id, będące intem będącym wartością zasobu typu id (czyli na przykład R.id.cośtam). Tego taga używa się wtedy, kiedy dla każdego widoku chcemy pamiętać coś. Na przykład jeśli w kalkulatorze dla każdego przycisku chcemy pamiętać, co on na sobie ma (jaką cyfrę lub działanie). Albo inny przykład jest opisany na http://www.pushing-pixels.org/2011/03/15/android-bits-and-pieces-view-tags.html . Tam opisują, jak mają widoki, którym muszą zmieniać lewy padding, żeby ich zawartość nie wchodziła na wężykową linię dzielącą ekran na pół. I żeby pamiętać, jaki był pierwotny lewy padding, to go trzymają w tagu właśnie. Mi się zdaje, że tag to jest sposób, żeby programować w Javie jakby to był Perl - dodawać obiektom jakie chcemy atrybuty, jakiego chcemy typu i o jakiej chcemy nazwie - i żeby kompilator nas nie pilnował. 88. Czasem Eclipse przy layoucie podpowiada: "set android:baselineAligned="false" for better performance". To dlatego, że - podobno, nie sprawdzałem - niektóre layouty wyrównują rysowane w sobie widoki tak, żeby linie podstawowe tekstu w tych widokach były spasowane. Jeśli tego nie potrzebujemy, możeby to wyłączyć, bo po co layout ma pracowicie robić coś, czego nie potrzebujemy. Źródło: http://stackoverflow.com/questions/9319916/how-does-setting-baselinealigned-to-false-improve-performance-in-linearlayout http://udinic.wordpress.com/tag/baselinealigned/ 89. W Androidzie przy findViewById trzeba rzutować. A mogli zrobić inaczej: żeby klasa R oprócz atrybutów "i nt R.id.coś" miała metody "typCosia R.viewById.getCoś(Context c)". 90. Skąd się bierze android.R.layout.simple_spinner_item (2012.02.11 22:55) Jak się tworzy spinner (znaczy, listę rozwijaną), trzeba napisać: ArrayAdapter adapterKategorii = ArrayAdapter.createFromResource(this, R.array.kategorie, android.R.layout.simple_spinner_item); Można się zastanawiać, skąd się bierze ten cały android.R.layout.simple_spinner_item. Jak się na niego kliknie w Eclipsie, to się pojawia informacja, że pochodzi on z pliku c:\Program Files\Android\android-sdk\platforms\android-4\android.jar. Jak się ma zainstalowany dekompilator jd-gui, to tego jara można sobie zdekompilować poleceniem "jd-gui android.jar", ale to nie jest ciekawe - bo się po prostu zobaczy, że w klasie android.r.layout (która jest wewnętrzną klasą w klasie android.R) jest statyczne pole typu int z jakąś czarodziejską liczbą będącą id tego layoutu. Sam layout jest w środu w jarze, w pliku resources.arsc (znaczy w pliku ze skompilowanymi zasobami). Jak się ma zainstalowany apktool, można sobie zdekompilować te zasoby poleceniem "apktool d android.jar". Wynikiem dekompilacji jest katalog, w którym trzeba na nos - no bo przecież nie po tym id wyczytanym z klasy R - poszukać odpowiedniego pliku z layoutem. Zresztą trudno go znaleźć nie jest, leży w res/layout/simple_spinner_item.xml. I już go można sobie obejrzeć. A jak chce się go zmienić, to trzeba w swoim projekcie stworzyć plik res/layout/moj_spinner_item.xml, w niego wkopiować treść pliku simple_spinner_item.xml (tego będącego wynikiem dekompilacji), usunąć zeń id, ładnie go sformatować, pozmieniać co się chce. A potem w kodzie napisać: ArrayAdapter adapterKategorii = ArrayAdapter.createFromResource(this, R.array.kategorie, R.layout.moj_spinner_item); 91. Próbuję dojść właśnie, jak leży ten układ odniesienia, według którego czujniki w Androidzie (na przykład czujnik pola magnetycznego czy akcelerometr) zgłaszają, co czują. Ten układ jest związany z telefonem - s łuszne i logiczne, bo czujniki są przymocowane do telefonu. Ale dalej mówią: "when a device is held in i ts natural orientation, the X axis is horizontal and points to the right, the Y axis is vertical and poin ts up". No dobra, ale jaka orientacja jest naturalna? Piszą: "usually for tablets, the natural orientat ion is landscape, while for phones, the default orientation is portrait". Nadal mi to niewiele mówi. Ja czasem telefon trzymam tak, że jego ekran leży poziomo, a czasem tak, że jego ekran leży pionowo - przy k tórym z tych położeń the Y axis is vertical and points up? Ale wreszcie to sobie ułożyłem w głowie i nini ejszym zapisuję. Trzeba siąść przy biurku i narysować na nim ołówkiem dwuwymiarowy układ współrzędnych z osiami X i Y jak w szkole - oś Y oddala się od mnie, oś X idzie od lewej ku prawej. Do tego wyobrazić sob ie oś Z prostopadłą do powierzchni biurka. A potem normalnie położyć na biurku telefon tak, żeby długość telefonu była wzdłuż osi Y. I to jest ten układ współrzędnych. 92. Czy na Androidzie szybciej narysować jeden duży czy kilka małych (2012.08.03 21:28) Zastanawiałem się wczoraj, czy na Androidzie szybciej rysuje się (przez Canvas.drawBitmap) jeden duży obrazek czy kilka małych obrazków (o łącznie tej samej powierzchni). Zmierzyłem i wyszło mi, że narysowanie jednego obrazka o rozmiarach 200x200 pikseli zajmuje od 2 do 21 ms, średnio 2.681 ms (mierzyłem czas prawdziwy, nie czas procesora - odejmowałem timestamp z po i przed narysowania). A narysowanie stu obrazków o rozmiarach 20x20 pikseli (więc łącznie mających tę samą powierzchnię) zajmuje od 5 do 63 ms, średnio 6.9265 ms. Więc wychodzi, że szybciej się rysuje jeden duży obrazek niż kilka małych. Potem sprawdziłem, czy prawdą jest, co mówią ludzie, że szybciej się rysuje, jeśli obrazek nie ma przezroczystości. Mi wyszło, że tak samo szybko rysują się obrazki z przezroczystością i bez przezroczystości (nawet jeśli te obrazki częściowo na siebie nachodzą). Potem sprawdziłem, czy prawdą jest, co mówią ludzie, że szybkość rysowania zależy od tego, w jakim formacie jest obrazek (robiłem coś jak obrazek.copy(Config.ARGB_4444, false)). Mi wyszło dziwnie - przy rysowaniu pojedynczego obrazka różnica była (format RGB_565 rysował się najszybciej, od 0 do 17 ms, średnio 1.154 ms), a przy rysowaniu wielu małych obrazków różnicy nie było. Oto źródło: (...) Zastanawiałem się wczoraj, czy na Androidzie szybciej rysuje się (przez Canvas.drawBitmap) jeden duży obrazek czy kilka małych obrazków (o łącznie tej samej powierzchni). Zmierzyłem i wyszło mi, że narysowanie jednego obrazka o rozmiarach 200x200 pikseli zajmuje od 2 do 21 ms, średnio 2.681 ms (mierzyłem czas prawdziwy, nie czas procesora - odejmowałem timestamp z po i przed narysowania). A narysowanie stu obrazków o rozmiarach 20x20 pikseli (więc łącznie mających tę samą powierzchnię) zajmuje od 5 do 63 ms, średnio 6.9265 ms. Więc wychodzi, że szybciej się rysuje jeden duży obrazek niż kilka małych. Potem sprawdziłem, czy prawdą jest, co mówią ludzie, że szybciej się rysuje, jeśli obrazek nie ma przezroczystości. Mi wyszło, że tak samo szybko rysują się obrazki z przezroczystością i bez przezroczystości (nawet jeśli te obrazki częściowo na siebie nachodzą). Potem sprawdziłem, czy prawdą jest, co mówią ludzie, że szybkość rysowania zależy od tego, w jakim formacie jest obrazek (robiłem coś jak obrazek.copy(Config.ARGB_4444, false)). Mi wyszło dziwnie - przy rysowaniu pojedynczego obrazka różnica była (format RGB_565 rysował się najszybciej, od 0 do 17 ms, średnio 1.154 ms), a przy rysowaniu wielu małych obrazków różnicy nie było. Oto źródło: package pl.psobolewski.testrysowania; import java.util.Date; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Paint; import android.view.View; public class MojWidok extends View { private final Paint paint = new Paint(); private final Bitmap obrazek; private final long[] czasy = new long[2000]; private int i = 0; private boolean koniec = false; public MojWidok(Context context) { super(context); obrazek = BitmapFactory.decodeResource(context.getResources(), R.drawable.test_200_200).copy(Config.ARGB_4444, false); //obrazek = BitmapFactory.decodeResource(context.getResources(), R.drawable.test_20_20_alfa); } @Override protected void onDraw(Canvas canvas) { if (koniec) return; Date teraz = new Date(); // for (int x = 0; x < 10; x++) { // for (int y = 0; y < 10; y++) { // canvas.drawBitmap(obrazek, 10 * x, 10 * y, paint); // } // } canvas.drawBitmap(obrazek, 10, 10, paint); Date potem = new Date(); if (i < czasy.length) { czasy[i++] = (potem.getTime() - teraz.getTime()); invalidate(); } else { long suma = 0; long max = 0; long min = Long.MAX_VALUE; for (long czas: czasy) { suma += czas; if (czas < min) min = czas; if (czas > max) max = czas; } System.out.println("sredni czas: " + (((double) suma) / czasy.length)); System.out.println("najkrótszy czas: " + min); System.out.println("najdłuższy czas: " + max); koniec = true; } } } 93. Jak się inflaterem nadmuchuje widok z XML-a z zasobów, to oprócz numerka zasobu (na przykład R.layout.mójlayout) podaje się też drugi parametr, którym ma być widok, który w przyszłości będzie rodzicem tego właśnie nadmuchiwanego widoku. Zawsze nie wiedziałem, po co się go podaje, i ja go nie podawałem, dawałem w tym drugim parametrze null i też działało. Wreszcie dowiedziałem się, o co z tym chodzi - a chodzi o to: Kiedy dodaje się widok do kontenera, trzeba podać obiekt klasy LayoutParams albo potomnej zawierający parametry dla kontenera określające sposób, w jaki kontener ma rozmieścić ten dodawany widok. Waniliowy LayoutParams ma tylko dwa atrybuty: width i height, określają one, jaką wysokość i szerokość ma dostać nasz widok w tym kontenerze. Stworzenie obiektu layoutparams i dostarczenie go kontenerowi może odbyć się na kilka sposobów. Można stworzyć obiekt layoutparams przez "new LayoutParams". Potem można ten obiekt layoutparams przekazać kontenerowi przy wywoływaniu metody ViewGroup#addView, obok dodawanego widoku. Ale można też wsadzić ten layout w widok, metodą View#setLayoutParams. Wtedy ten widok niesie w sobie layoutparams z ustawieniami, jak chce być włożony w kontener, i można metodzie ViewGroup#addView przekazać tylko go (czyli tylko ten widok), a nie przekazywać już obiektu layoutparams. Można też napisać parametry dla layoutu w pliku XML z layoutem, jako iksemelowe atrybuty o nazwach layout_cośtam. Tylko z tym pisaniem parametrów lejałtowych w pliku XML z layoutem wiąże się ciekawy problem. Bo poszczególne kontenery (różne klasy dziedziczące po ViewGroup) tworzą własne klasy dziedziczące po LayoutParams (one zawsze - taki zwyczaj - są statycznymi klasami wewnętrznymi siedzącymi w tych klasach dziedziczących po ViewGroup i nadal nazywają się LayoutParams - więc jest na przykład klasa LinearLayout.LayoutParams) i rozbudowujące je o dodatkowe atrybuty. Na przykład klasie LinearLayout towarzyszy klasa LinearLayout.LayoutParams, w której oprócz zwykłych atrybutów width i height jest jeszcze atrybut gravity mówiący, w którą stronę ma być dosunięty dany element. Więc jak inflater nadmuchuje dany widok z XML-a, a autor tego XML-a umieścił w nim różne atrybuty lejałtowe, to pewnie trzeba by te atrybuty lejałtowe umieścić nie w obiekcie waniliowej klasy LayoutParams, tylko w którejś z klas dziedziczących po LayoutParams. Ale jakiej? Żeby to wiedzieć, trzeba wiedzieć, w jakim kontenerze będzie siedzieć ten nadmuchiwany element. Kiedy inflater nadmuchuje XML-a, to zwykle w tym XML-u jest korzeń i jego potomkowie. Z potomkami nie ma żadnego problemu, bo dla każdego z nich w pliku XML widać, jakiej klasy jest jego rodzic, więc inflater może wybrać odpowiednią klasę LayoutParams. Ale z korzeniem tak się nie da - przy nadmuchiwaniu korzenia inflater musi wiedzę o klasie, której obiekt ma stworzyć, dostać z zewnątrz, albo nie stworzy obiektu layoutparams z parametrami lejałtowymi. I to dlatego przy nadmuchiwaniu lejałtu z XML-a możemy metodzie inflate przekazać obiekt typu ViewGroup, w którym będzie później umieszczony korzeń. Jeśli nie przekażemy go (czyli jeśli zamiast niego przekażemy null), to inflater nie wczyta dla korzenia parametrów lejałtowych i nie stworzy obiektu layoutparams z nimi i nie umieści tego obiektu layoutparams w tworzonym widoku. A jeśli przekażemy metodzie inflate ten kontener, w którym będzie umieszczony korzeń, to mamy dwie możliwości. Możemy metodzie inflate przekazać jeszcze trzeci parametr mówiący, czy inflater ma od razu podczepić nadmuchany korzeń temu rodzicowi. Jeśli w tym trzecim parametrze przekażemy true albo nie przekażemy go wcale (czyli wywołamy dwuargumentową wersję metody inflate), inflater nadmucha korzeń, wyciągnie z jego XML-a parametry lejałtowe, na kontenerze, w którym ma być umieszczony korzeń, zawoła metodę generateLayoutParams i przekaże jej te parametry lejałtowe wyciągnięte z XML-a, a metoda generateLayoutParams zwróci obiekt odpowiedniej klasy dziedziczącej po LayoutParams z zaszytymi w nim parametrami lejałtowymi, i wtedy inflater doda korzeń do tego kontenera przekazując ten obiekt layoutparams, i zwróci wcale nie, jak można by myśleć, widok będący wynikiem nadmuchania XML-a (czyli korzeń), tylko kontener, w którym umieszczony został korzeń. A jeśli przekażemy ten parametr i przekażemy w nim false, to inflater nadmucha korzeń, wyciągnie z jego XML-a parametry lejałtowe, na kontenerze, w którym ma być umieszczony korzeń, zawoła metodę generateLayoutParams i przekaże jej te parametry lejałtowe wyciągnięte z XML-a, a metoda generateLayoutParams zwróci obiekt odpowiedniej klasy dziedziczącej po LayoutParams z zaszytymi w nim parametrami lejałtowymi, a wtedy inflater weźmie ten obiekt i umieści go w korzeniu wołając na nim setLayoutParams i taki nadmuchany korzeń z zaszytym w nim obiekcie layoutparams zwróci. Dzięki czemu my, dostawszy ten nadmuchany obiekt, jak zechcemy go umieścić w kontenerze, to będziemy mogli go w tym kontenerze umieścić metodą addView nie zajmując się obiektem layoutparams - bo ten obiekt jest już w nadmuchanym obiekcie umieszczony. Skoro ten kontener jest używany tylko do tego, żeby powiedzieć, jakiej klasy ma być obiekt layout params, to nasuwało mi to dwa powiązane ze sobą pytania. Po co przekazuje się obiekt tej klasy, a nie obiekt klasy Class reprezentujący samą klasę? Znaczy, dlaczego się nie pisze getLayoutInflater().inflate(R.layout.mójlayout, LinearLayout.class)? I czy byłoby to samo, jakbym przekazał nie ten kontener, w którym naprawdę potem umieszczę ten korzeń, a inny obiekt tej samej klasy? Wydaje mi się - choć dokładnie tego nie sprawdzałem - że przecież nieraz chcemy, żeby nadmuchując widok inflater na atrybuty z XML-a nakładał atrybuty z motywu, a do tego jest potrzebny kontekst, a ten kontekst może być wzięty z tego kontenera. Ale to jest tylko domysł, dokładnie tego nie sprawdzałem. Przykłady do tego, co tu napisałem, są w kurs-android/notatki-psobolewski/290-layout_params_i_nadmuchiwanie. 94. Żeby w Android Studio dodać do projektu jara, trzeba najpierw stworzyć w projekcie katalog libs (chyba mógłby się też nazywać jakkolwiek inaczej), który ma być bratem katalogów src i build. Do niego wkopiować jara. Prawym na projekcie, "open module settings", modules, zakładka dependencies, w niej dodać jara. Nie dziwić się, jeśli po kliknięciu "ok" wydaje się, że się nie dodało - dodało się, tylko nie od razu widać, bo mieli. Ustawienie "scope" można zostawić na "compile", tak jak jest domyślnie. To wszystko. W sieci są opisy, które każą ręcznie edytować pliki gradle'a i robić inne cyrki, ale to podobno w starych wersjach tak trzeba było robić, a teraz - widać - nie. 95. Oto, jak działają loadery. Czasem aplikacja potrzebuje załadować jakieś dane. Zajmują się tym loadery. Taki loader to obiekt (klasy android.content.Loader) (tak naprawdę to nie jest obiekt, ale grupa obiektów o wspólnej konfiguracji, ale dobra, nie dzielmy włosa na czworo), który ma metodę "załaduj" (dokładnie: onForceLoad). Ta metoda jakoś ładuje dane. Znaczy, w samej klasie Loader ta metoda jest pusta, a klasy potomne mają ją nadpisać, żeby ładowała odpowiednie dane odpowiednim sposobem. I można by pomyśleć, że ta metoda zwraca załadowane dane - ale nie, bo jakby tak prosto było, to nie dałoby się zrobić, żeby ładowanie odbywało się w bocznym wątku, a zwrócenie danych do aplikacji w głównym. A więc ta metoda rozpoczyna ładowanie, a jak już dane zostaną załadowane, to loader ma obowiązek zawołać na sobie metodę deliverResult. Ta metoda dostarcza dane do aplikacji. Robi to tak, że każdy, kto chce, może zarejestrować się w loaderze jako słuchacz i zostanie powiadomiony, jak dane będą gotowe. Taki słuchacz musi spełniać interfejs LoaderCallbacks, musi mieć metodę onLoadFinished i ta własnie metoda onLoadFinished zostaje wywołana na słuchaczu przez metodę deliverResult loadera. Więc bardzo prosty loader mógłby działać tak, że w metodzie onForceLoad by załadował dane (w głównym wątku), po czym zwrócił je wołając deliverResult. Nie ma sensu przy każdym obróceniu telefonu tworzyć na nowo loadera i na nowo ładować dane - więc każda aktywność i każdy fragment mają LoaderManagera, który opiekuje się loaderami. Ten loader manager ma metodę initLoader, która służy do tego, żeby stworzyć nowy obiekt loadera (chyba, że już istnieje - bo na przykład przed obrotem telefonu istniał - to użyć istniejącego) i oddać go pod opiekę loader managerowi. Tej metodzie podaje się kilka drobiazgów oraz fabrykę, której loader manager użyje do stworzenia loadera, jeśli będzie potrzebna. Ta fabryka - kto by pomyślał - implementuje też interfejs LoaderCallbacks. Więc interfejs LoaderCallbacks z jednej strony opisuje słuchacza, który jest powiadamiany, jak dane są już gotowe, a z drugiej strony opisuje fabrykę, której loader manager użyje do stworzenia dla nas loadera. Ten słuchacz o jeszcze jednej rzeczy może być powiadomiony, więc jeszcze jedną metodę muszą mieć implementujące go klasy: metodę onLoaderReset. Ta metoda będzie wołana, kiedy loader będzie niszczony, ale nie bardzo wiem, kto miałby go niszczyć i dlaczego. W źródłach widzę, że loader manager ma metodę destroyLoader, która to metoda niszczy zadanego loadera i na jego słuchaczu woła onLoaderReset, ale niemal nie widziałem - ani w praktyce, ani w źródłach androida - żeby ktoś tę metodę wołał. No, tak czy siak muszę tę metodę zaimplementować implementując interfejs LoaderCallbacks. No więc metoda init loader managera, jak mówiłem, tworzy loadera i woła na nim metodę "załaduj". Chyba że ten obiekt już istnieje, to nie tworzy drugiego, ani na starym nie woła "załaduj", tylko bierze zapamiętany stary wynik, otrzymany kiedyś od tego starego obiektu przez deliverResult, i znowu go daje obiektowi LoaderCallbacks, jakby te dane właśnie przed chwilą loader wytworzył. Zwykle chcemy, żeby załadowanie danych odbywało się w osobnym wątku. Jest do tego gotowa klasa - android.content.AsyncTaskLoader. W tej klasie jest zrobione, że metoda onForceLoad odpala async taska (a dokładniej to ta klasa sama jest async taskiem) i w doInBackground tego async taska uruchamia metodę loadInBackground. A co ta metoda loadInBackground zwróci, jest potem przekazywane do deliverResult. W samej klasie AsyncTaskLoader metoda loadInBackground jest pusta, mają ją nadpisać klasy potomne - w ten sposób określając, jak mają być ładowane dane. Jedną z klas dziedziczących po AsyncTaskLoader jest klasa CursorLoader. To jest loader, któremu dajemy URI do danych, a on w bocznym wątku robi query na tym URI i dostarcza otrzymany kursor. Ten loader w konstruktorze rejestruje się jako słuchacz w swoim kursorze, żeby jak się kursor dowie od ContentManagera, że dane się zmieniły, to żeby kursor powiadamiał CursorLoadera, a wtedy CursorLoader przeładowuje (w AsyncTasku) dane. 96. Jak mam swoją klasę dziedziczącą po SQLiteOpenHelper i tworzę jej instancję, to muszę konstruktorowi przekazać kontekst. Ale jeśli przekażę tam nulla, to najpierw nie będzie żadnego błędu. Dopiero jak potem zrobię SQLiteOpenHelper#getReadableDatabase(), to dostanę NPE, bez żadnej podpowiedzi, za co to NPE dostaję. Więc na przykład jeśli spróbuję stworzyć obiekt SQLiteOpenHelper w konstruktorze content providera i w tym konstruktorze zrobię coś takiego: Db db = new Db(getContext()); to przejdzie to bez błędu. Mimo że w konstruktorze getContext() zwraca jeszcze nulla (trzeba by to robić w onCreate a nie w konstruktorze). I błąd będzie dopiero potem, kiedy będę potrzebował pobrać jakieś dane z bazy. 97. Jak się używa klasy CursorAdapter, to nieraz trzeba zdecydować, czy zaimportować ją normalnie, z android.widget, czy też z android.support.v4.widget. A potem jeszcze trzeba zdecydować, którego z kilku konstruktorów będziemy używać. Chodzi w tym wszystkim o to, że kiedyś CursorAdapter podłączał się jako słuchacz do swojego kursora, żeby kursor go powiadamiał, kiedy dowie się od ContentManagera, że zasilające go dane zmieniły się, i jak się dowiedział, że się zmieniły, ponownie pobierał dane. I robił to w głównym wątku. To nie był dobry pomysł, bo przecież długo trwających działań - takich jak pobranie danych z bazy - nie powinniśmy robić w głównym wątku. Więc w API lewelu 11 zdeprekowano takie zachowanie. Dodano dwa nowe konstruktory pozwalające określić, czy chcemy, żeby CursorAdapter dociągał dane w głównym wątku, a staremu kursorowi nie zmieniono zachowania, ale go zdeprekowano. Dlatego jeśli chcemy pisać porządnie, powinniśmy używać tylko nowych, niezdeprekowanych konstruktorów, co wymaga, że jeśli za minimalną obsługiwaną wersję Androida ustawiamy wersję niższą niż 11, musimy wziąć wersję CursorAdaptera z android.support.v4.widget. Na przykład można użyć konstruktora przyjmującego flagi i jako flagi przekazać zero - brak flag powoduje zachowanie dobre (że CursorAdapter nie przeładowuje danych w głównym wątku). Tylko wtedy trzeba jakoś inaczej zadbać o to, żeby dane zostały przeładowane, kiedy się zmienią. Na przykład przez użycie loader managera. 98. Jak się chce, żeby content provider udostępniał plik, to trzeba w content providerze przykryć metodę openFile, na przykład tak: @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { File f = new File(getContext().getCacheDir(), "tmp"); FileOutputStream os = new FileOutputStream(f); OutputStreamWriter osw = new OutputStreamWriter(os); try { osw.write("ala\nma asa\n"); osw.close(); } catch (IOException e) { throw new RuntimeException(e); } return ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY); } No i oczywiście zarejestrować tego providera w deskryptorze: I potem gdzie chcemy - na przykład w aktywności - poprosić o strumień: try { InputStream is = getContentResolver().openInputStream(Uri.parse("content://hop.siup")); BufferedReader isr = new BufferedReader(new InputStreamReader(is)); String linia = isr.readLine(); Log.i("@@@", "wczytałem: " + " " + linia); } catch (Exception e) { throw new RuntimeException(e); } Tu było dla mnie nieintuicyjne, że w jednym miejscu daję ParcelFileDescriptor, a w drugim dostaję strumień. A ten ParcelFileDescriptor to jest takie Parcelable, które umie mieć zapakowany uchwyt do pliku i umie zserializować się do parceli razem z tym uchwytem - w taki czarodziejski sposób, że jak ktoś w innym procesie rozserializuje ParcelFileDescriptor, to będzie mógł normalnie używać tego deskryptora pliku. Co jest niezłą magią i nie mogłoby oczywiście być zrobione, gdyby nie było wspomagane przez jądro. A to wspomaganie jest robione przez natywne metody nativeWriteFileDescriptor i nativeReadFileDescriptor w klasie Parcel. 99. Jak aplikacja androidowa rzuci Runtime Exception, w logach pojawia się stacktrace. Ale jeśli stacktrace jest długi, nie pojawiają się jego wszystkie linie. Wtedy przydaje się wiedzieć, że metody Log.{i,d,e,itp} mają wersję trzyargumentową: trzecim argumentem jest dowolny wyjątek, a w logach pojawia się jego stacktrace. Więc żeby zalogować aktualny stacktrace, wystarczy zrobić Log.i("coś", "tralala", new Exception()). 100. Czasem zapominam, jak to jest z wyjątkami w FutureTasku. Zwłaszcza dlaczego FutureTask#get może rzucać aż trzy rodzaje wyjątków, kiedy rzuca jaki i w ogóle. Dobrym przykładem, żeby to sobie poukładać, jest taki: public class Test { public static void main(String[] args) { System.out.println("kuku!"); FutureTask t = new FutureTask(new Callable() { @Override public Integer call() throws Exception { System.out.println("zaczynam obliczenia"); try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("w trakcie spania w tasku był wyjątek typu: " + e.getClass().getCanonicalName()); } System.out.println("kończę obliczenia"); return 3 / 1; } }); ExecutorService es = Executors.newSingleThreadExecutor(); es.execute(t); try { Thread.sleep(500); } catch (InterruptedException e) { System.out.println("w trakcie spania w tasku był wyjątek typu: " + e.getClass().getCanonicalName()); } System.out.println("obliczenia " + (t.isDone() ? "" : "nie") + " są zakończone"); // t.cancel(true); try { System.out.println("wynik: " + t.get()); } catch (Exception e) { System.out.println("w trakcie geta był wyjątek typu: " + e.getClass().getCanonicalName()); if (e instanceof ExecutionException) { ((ExecutionException) e).printStackTrace(); } } } } W takiej postaci ten program wyświetla: kuku! zaczynam obliczenia obliczenia nie są zakończone kończę obliczenia wynik: 3 Jak widać, główny wątek uruchamia futuretaska w bocznym wątku, w metodzie get czeka na wynik, aż go dostaje. Jeżeli "return 3 / 1" zmienimy na "return 3 / 0", program da taki wynik: kuku! zaczynam obliczenia obliczenia nie są zakończone kończę obliczenia w trakcie geta był wyjątek typu: java.util.concurrent.ExecutionException java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero at java.util.concurrent.FutureTask$Sync.innerGet(Unknown Source) at java.util.concurrent.FutureTask.get(Unknown Source) at Test.main(Test.java:36) Caused by: java.lang.ArithmeticException: / by zero at Test$1.call(Test.java:23) at Test$1.call(Test.java:1) at java.util.concurrent.FutureTask$Sync.innerRun(Unknown Source) at java.util.concurrent.FutureTask.run(Unknown Source) at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) at java.lang.Thread.run(Unknown Source) Jak widać, metoda call może rzucić dowolny wyjątek. Jeśli go rzuci, to ten, kto uruchamia call, zapakowuje ten wyjątek (nawet jeśli jest to, jak tu, Runtime Exception) w ExecutionException i wyrzuca ten ExecutionException z metody get. Jeśli na próbę zakomentujemy ten fragment wołający get, żadnego wyjątku nie zobaczymy - ten wyjątek z metody call zostanie połknięty. Znaczy, jeśli ten fragment zakomentujemy: try { System.out.println("wynik: " + t.get()); } catch (Exception e) { System.out.println("w trakcie geta był wyjątek typu: " + e.getClass().getCanonicalName()); if (e instanceof ExecutionException) { ((ExecutionException) e).printStackTrace(); } } Wynik wtedy jest taki: kuku! zaczynam obliczenia obliczenia nie są zakończone kończę obliczenia Można też wziąć niezmienioną wersję tego przykładu i odkomentować w nim polecenie "t.cancel(true)". Wtedy wynik będzie taki: kuku! zaczynam obliczenia obliczenia nie są zakończone w trakcie geta był wyjątek typu: java.util.concurrent.CancellationException w trakcie spania w tasku był wyjątek typu: java.lang.InterruptedException kończę obliczenia Ciekawe jest, że - zauważmy - najpierw get zakończył działanie rzucając wyjątek CancellationException, a dopiero po chwili metodę call (a dokładnie sleep w niej) spróbowano przerwać. Gdybyśmy nie połykali wyjątku wypadającego ze sleepa, tylko go - jak to się nieraz porządnie robi - wyrzucili w górę, robienie obliczeń zostałoby przerwane i nie pojawiłby się napis "kończę obliczenia". Jeszcze inaczej byłoby, gdybyśmy "t.cancel(true)" zmienili na "t.cancel(false)". Ten argument Boolowski mówi, czy metoda cancel ma spróbować interruptnąć metodę call, jeśli zaczęła się ona wykonywać. Więc jeśli damy w nim false, get natychmiast rzuci wyjątek CancellationException, ale call wykona się bez przerw do końca. Co da output taki: kuku! zaczynam obliczenia obliczenia nie są zakończone w trakcie geta był wyjątek typu: java.util.concurrent.CancellationException kończę obliczenia Jak widać, tym razem sleep w metodzie call nie został przerwany. Jeszcze jeden wyjątek mógłby wylecieć z metody get. Jak przy każdej metodzie blokującej, gdyby ktoś przerwał wątek, w którym dzieje się get, w czasie, kiedy dzieje się get, wyleciałby z niego InterruptionException. 101. Jak chcę mieć obrazek wyświetlający bitmapę i żeby rozmiar obrazka był większy od rozmiaru bitmapy i żeby bitmapa go wypełniła powtarzając się, to trzeba ten obrazek wyświetlać nie widokiem ImageView, ale zwykłym gołym View: I potem stworzyć obiekt klasy BitmapDrawable z danym obrazkiem, ustawić mu, żeby się powtarzał, i ustawić to bitmapdrawable jako tło temu view: BitmapDrawable bitmapDrawable = new BitmapDrawable(context.getResources(), to); bitmapDrawable.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); iv.setBackgroundDrawable(bitmapDrawable); Tu można się zdziwić, bo View ma dwie podobne metody: setBackgroundDrawable, która jest zdeprekowana, i setBackground, która jest dopiero od API wersji 16. Co dziwniejsze, w źródłach Androida zobaczymy, że metoda setBackground wygląda tak: public void setBackground(Drawable background) { //noinspection deprecation setBackgroundDrawable(background); } Chodzi o to, że twórcy Androida metodę setBackgroundDrawable wycofali tylko ze względów estetycznych - skoro w XML-u odpowiedni atrybut nazywa się android:background, to dlaczego by setter miał się nazywać setBackgroundDrawable? Ale poza tym obie metody działają tak samo, więc jeśli nasza aplikacja ma być uruchamialna poniżej API wersji 16, wygodniej używać starej metody. 102. Klasa Resources to - choć nazwa może tego nie sugerować - menadżer zasobów. Jakbym ja robił Androida, to bym ją nazwał ResourcesManager. 103. Oto przykład, jak skorzystać z czegoś, co jest tylko w nowszych wersjach API, ale dopuścić, żeby aplikacja była uruchamiana na starszych wersjach API. Na przykład, jak w metodzie onCreateOptionsMenu tworzę pozycję menu, to mogę ustawić, żeby ta pozycja była pokazywana nie w starym menu na dole ekranu, ale po nowemu - w action barze. Robi się to tak: @Override public boolean onCreateOptionsMenu(Menu menu) { MenuItem mi = menu.add("kuku"); mi.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); return true; } Ale i metoda setShowAsAction, i stała MenuItem.SHOW_AS_ACTION_ALWAYS pojawiły się od API 11. Jeśli piszę aplikację, która ma być instalowalna od niższej wersji API (bo tak ma ustawione w manifeście), nie mogę zostawić tego fragmentu ot tak po prostu. Na pewno trzeba otoczyć go sprawdzeniem wersji API: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { (...) } Ale co w tym ifie w środku napisać? Kiedyś trzeba było (i niektóre opisy w sieci nadal tak każą) dać w środku wywołanie metody przez introspekcję. Bo przecież jeśli damy wywołanie normalnie, nie przez introspekcję, to - tak by się mogło wydawać - telefon nawet nie spróbuje tego kodu wykonywać. Bo przy ładowaniu klasy wirtualna maszyna Javy sprawdza, czy kod w tej klasie nie wywołuje z innych klas metod, których by te inne klasy nie miały, i jeśli wywołuje, to wirtualna maszyna javy odmawia załadowania tej klasy - rzuca wyjątek. I rzeczywiście tak to działało na starych telefonach - przed API 5. Ale teraz to działa inaczej. Takie rzeczy nie są sprawdzane przy ładowaniu klasy, ale dopiero w czasie wykonywania. O czym można się przekonać próbując uruchomić taki kod: if (Math.random() > 10) { mi.setShowAsAction(2); } A więc jeśli nie piszemy aplikacji na telefony starsze niż API 5 (a przecież nie piszemy, to staroć), możemy w tym ifie nowe metody wywoływać normalnie, a nie przez introspekcję. Tylko że jest jeszcze jeden problem - coś, ktoś (chyba lint - tak sugeruje http://developer.android.com/reference/android/annotation/TargetApi.html ; więcej o lincie w SDK piszą tu: http://tools.android.com/tips/lint , http://tools.android.com/tips/lint/suppressing-lint-warnings , http://tools.android.com/tips/lint-checks) nie pozwala skompilować programu, jeśli wywołuje on metody niedostępne w minSdkVersion. Żeby taki kod przepchnąć przez kompilator, trzeba odpowiednią metodę oznaczyć anotacją @TargetApi(Build.VERSION_CODES.HONEYCOMB) (zresztą eklips sam to podpowiada). Ta anotacja mówi, że w tej metodzie ma być dozwolone to, co można robić od Honeycomba w górę. Podsumowując - trzeba napisać tak: @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override public boolean onCreateOptionsMenu(Menu menu) { MenuItem mi = menu.add("kuku"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { mi.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); } return true; } Żeby nie pojawiało się ostrzeżenie przy korzystaniu z atrybutu SHOW_AS_ACTION_ALWAYS, trzeba dodać anotację @SuppressLint("InlinedApi"). Zresztą, i ją też eklips podpowiada. Tę anotację piszemy dla linta. Dowodem na to, że lint ją rozumie, jest że jak się wykona polecenie sdk\tools\lint.bat --list, to na liście obsługiwanych anotacji pojawi się i ta. Źródła: http://developer.android.com/training/basics/activity-lifecycle/starting.html (pisze tam: "Using the SDK_INT to prevent older systems from executing new APIs works in this way on Android 2.0 (API level 5) and higher only. Older versions will encounter a runtime exception") oraz http://stackoverflow.com/questions/6891176/is-reflection-necessary-if-i-use-if-android-os-build-version-sdk-int-11 104. Klasa MenuItem ma atrybut SHOW_AS_ACTION_ALWAYS. Chciałem ostatnio sprawdzić, co będzie, jeśli spróbuję go wyświetlić i uruchomię taką aplikację na telefonie z API 8. Napisałem taki program: @Override protected void onCreate(Bundle savedInstanceState) { (...) Log.i("@@@", "wersja api to " + Build.VERSION.SDK_INT); Log.i("@@@", "MenuItem.SHOW_AS_ACTION_ALWAYS = " + MenuItem.SHOW_AS_ACTION_ALWAYS); } Uruchomiłem go na emulatorze na maszynie z API 8. W logach pojawiło się, że wersja API to 8, a ten atrybut ma wartość 2. Zdziwiłem się, bo myślałem, że na API 8 to się wywali. Zdeasemblowałem więc ten program i zobaczyłem w nim: (...) const-string v0,"@@@" const-string v1,"MenuItem.SHOW_AS_ACTION_ALWAYS = 2" invoke-static {v0,v1},android/util/Log/i ; i(Ljava/lang/String;Ljava/lang/String;)I A! Znaczy kompilator wypalił w programie wartość tego pola. 105. Na Androidzie w standardowej bibliotece są dwie klasy do łączenia się po HTTP - URLConnection i android.net.http.AndroidHttpClient. Mi tam różnice między nimi nie wydają się wielkie, ale ludzie z Androida radzą, żeby na starszych telefonach używać raczej AndroidHttpClient, a na nowszych - URLConnection. Źródło: http://android-developers.blogspot.co.nz/2011/09/androids-http-clients.html , http://stackoverflow.com/questions/9551058/urlconnection-or-httpclient-which-offers-better-functionality-and-more-efficie 106. Android studio swoje rzeczy trzyma nie tylko w katalogu, w którym go zainstalowaliśmy. Niektóre swoje rzeczy trzyma gdzie indziej, co potrafi zmylać - coś nie działa, więc odinstalowuję Android Studio, kasuję katalog instalacyjny i instaluję od nowa - a tu jednak były pozostałości po starej instalacji. A trzyma Android Studio swoje rzeczy w pewnych podkatalogach w katalogu domowym użytkownika. A pod Windowsami katalog domowy użytkownika to C:\Users\użytkownik. W nim Android Studio swoje rzeczy trzyma w katalogach .gradle i .AndroidStudioPreview. Może jeszcze w jakichś, ale te dwa znalazłem. Jeśli byśmy chcieli te .gradle i .AndroidStudioPreview mieć gdzie indziej - bo na przykład mamy mało miejsca na C - to konfiguruje się to w kataloginstalacyjnyandroidstudio\bin\idea.properties 107. Kiedyś myślałem (chyba F. to powiedział), że jest tak: W nowych androidach menu jest przenoszone do action bara. Więc jeśli napiszemy program i przez odpowiednie ustawienie w nim targetSdkVersion oznaczymy, że wiemy, jak zachowuje się on w nowych androidach, menu będzie przeniesione do action bara. Ale jeśli targetSdk damy niskie, menu do action bara nie zostanie przeniesione. Tylko uwaga, na telefonie się tego nie zauważy, bo domyślnie pozycje menu nie są przenoszone do głównej części action bara, tylko do jego overflow area, czyli tej części, która w telefonach wygląda jak menu i jest pokazywana guzikiem menu. Podobno na tabletach byłoby to widać, bo tam overflow area jest odsłaniane przyciskiem (wyświetlanym, nie hardłerowym), który pokazuje się na action barze. Właśnie przetestowałem to na tablecie. I widzę, że niezależnie od targetSdkVersion menu opcji zachowuje się na tablecie z nowym androidem tak samo: jest umieszczane w obszarze overflow actionbara, a ten obszar odsłania się niefizycznym przyciskiem, który jest w prawej części actionbara. 108. W klasie ActionBarActivity jak się przykrywa metodę onCreate, to trzeba w niej wywołać onCreate z rodzica - jak w każdej Activity. Ale inaczej niż w zwykłych activity, nie można tego zrobić w dowolnym miejscu metody onCreate - trzeba to zrobić przed zawołaniem setContentView. Jeśli się zrobi po, to dostanie się NPE ze środka ActionBarActivity#setContentView. Więc tak jest dobrze: public class AktywnoscTestowa extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new TextView(this)); } } a tak jest źle: public class AktywnoscTestowa extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { setContentView(new TextView(this)); super.onCreate(savedInstanceState); } } Dokumentacja ( https://developer.android.com/reference/android/support/v7/app/ActionBarActivity.html#onCreate%28android.os.Bundle%29 ) nie mówi o tym. Ona nawet nie mówi, że przykrywając onCreate w ActionBarActivity trzeba wywywołać onCreate z rodzica. 109. Uwaga przy tworzeniu PendingIntenta przez statyczne matody PendingIntent.getCoś. Jeśli kiedyś był już tworzony pending intent z takim samym intentem, nie jest tworzony nowy pending intent, tylko jest ponownie używany stary. A przy sprawdzaniu, czy osadzony intent jest taki sam jak jakiś już użyty, porównywana jest akcja, kategorie itp, ale nie extry. Więc może być taki efekt, że jak razy stworzymy jakiegoś pendingintenta, a potem innego pendingintenta różniącego się tylko extrami, może być taki efekt, jakby stare extry były skeszowane. Rozwiązaniem problemu jest podać przy tworzeniu pending intenta w czwartym parametrze flagi FLAG_CANCEL_CURRENT. Tu jest koniec listy nieoczywistych rzeczy. Ogólny obraz: Aplikacja składa się z Activities, które służą do wyświetlania ekranów aplikacji na ekranie telefonu. Wyświetlaniem zajmują się klasy dziedziczące po View. Każde Activity posiada jedno okno (Window), na którym mozna umieścić jedno (częściej) lub kilka (rzadziej) widoków (View). Ale na co dzień o oknie (Window) się nie myśli - sam Activity też ma metodę pozwalającą podłączyć do niego obiekt klasy View (lub ich kilka). Taki widok może być widokiem prostym (pole tekstowe, kółko postępu, mój własny widok wyświetlający grę) lub widokiem przechowującym inne widoki, czyli layoutem. Taki obiekt klasy layout mogę zainstancjonować samemu i samemu poustawiać mu właściwości i wypełnić go widokami, którym też sam poustawiam właściwości, mogę też nadmuchać go na podstawie pliku xml z opisem layoutu i tego, co w nim. Z kolei widok prosty jak robię, to on ma metodę onDraw, która dostaje obiekt klasy Canvas, który to obiekt służy do rysowania - ma metody rysuj kółko, rysuj kwadrat itp. Ewentualnie w onDraw można się wspomóc obiektami dziedziczącymi po Drawable - to są obiekty, co jak im się da płótno, to one umieją się na tym płótnie narysować. Takie drawable można sobie nadmuchiwać z zasobów. Cyklem życia Activity intensywnie zarządza Android - nieraz go niszczy, nieraz go tworzy, więc stan aplikacji warto trzymać w osobnym modelu, można też przekazywać go sobie przez onretainnonconfiguration. Jak trzeba zrobić coś, co trwa dłużej, robi się to na przykład przez stworzenie klasy dziedziczącej po AsyncTask. Widok (i obiekty przezeń tworzone) nieraz potrzebuje dostępu do jakichś androidowych rzeczy (na przykład zasobów, assetów, katalogu, w którym możemy trzymać nasz kesz itp). W Androidzie nie ma wstrzykiwania zależności, więc jest zrobione tak. Activity ma dostęp do tych rzeczy androidowych, bo jest stworzony przez Androida. Tworząc widoki Activity przekazuje im (do konstruktora) referencję na coś klasy Context - na siebie lub na swój ApplicationContext - i widok jak potrzebuje czegoś androidowego, to dostaje się do tego przez otrzymany kontekst. Przez kontekst można dostawać rózne serwisy, na przykład WindowManagera, który zarządza oknem, a przez niego Display, który rządzi wyświetlaczem. Oto, o co chodzi w Parcel, Parcelable i Bundle. Czasem trzeba przechować gdzieś serię wartości różnych typów. Na przykład opis osoby: jej imię i nazwisko (jako napisy, obiekty klasy String), jej wagę (jako prosty typ int) i jej wzrost (jako prosty typ double). Do przechowywania takich serii wartości służą obiekty klasy Parcel (dokładniej: parcela jest to paczka danych przesyłanym między procesami przy korzystaniu z bindera). Ta klasa ma pod spodem kawałek pamięci, do których metodami writeInt, writeLong itp można zapisywać dane dowolnych typów. Te metody nie oznaczają w pamięci, do której zapisują, jakiego typu jest to, co zapisują. Więc można zapisać rzecz jednego typu a odczytać jako inny typ lub zapisać rzeczy w jednej kolejności (na przykład int, long), a odczytać w innej kolejności (na przykład long, int) - dostaje się wtedy śmieci. Programista musi sam pilnować, żeby odczytywać te typy i w tej kolejności, co zapisywał. Ale czasem chcemy zapisywać obiekty. Jak się zapisuje i odczytuje daną klasę, zdecydować mogą tylko jej twórcy. Dlatego jeśli obiekty klasy mają być zapisywalne do parceli, muszą mieć metodę writeToParcel. Do oznaczania, że klasa ma tę metodę, służy interfejs Parcelable. Z kolei odczyt obiektu z parceli to nie jest rzecz robiona przez konkretny obiekt, bo obiekt powstanie dopiero w wyniku tego odczytania. Więc to musiałaby być metoda statyczna, a interfejs w Javie nie może nakazywać posiadania metody statycznej. Więc twórcy Androida zrobili dziwnie - zdecydowali, że każda klasa implementująca Parcelable musi mieć statyczny atrybut CREATOR zawierający obiekt (implementujący Parcelable.Creator) umiejący odczytywać z parceli obiekt i tablicę obiektów. To wymaganie nie jest (bo jakby miało być) zapisane w interfejsie Parcelable - jest tylko opisane w dokumentacji, poza tym jak spróbujemy użyć parcelabla bez CREATORA, dostaniemy wyjątek w czasie wykonania. Do tego interfejs Parcelable każe mieć metodę describeContents. Trochę niejasne jest dla mnie, po co ta metoda jest. Dokumentacja mówi mało, w kodzie klasy Parcel nic nie wywołuje tej metody. Ludzie mówią, że normalnie ta metoda powinna zwracać 0 (tak zresztą podpowiada Eclipse), a coś innego tylko wtedy, jeśli zapisywany obiekt zawiera uchwyt do pliku. Do parceli da się zapisać obiekt anonimowej klasy implementującej Parcelable, ale nie da się go potem odczytać - próba odczytania daje wyjątek. Ponieważ zapisywanie prymitywów do parceli robi się przez wywołanie metody na parceli (a nie na prymitywie), więc dla konsekwencji Parcel ma też metodę writeParcelable, która najpierw wpisuje do tej pamięci, do której zapisuje, nazwę klasy, a następnie woła na obiekcie writeToParcel. Jest też metoda writeValue, która umie zapisać do parceli dowolny prymityw lub obiekt klasy Parcelable - oznaczając w pamięci, do której zapisuje, jakiego typu jest to, co zapisuje (magicznym intem zapisanym przed zapisaną wartością). Ta metoda writeValue umie też zapisywać kilka innych rzeczy (ich lista jest zaszyta w kodzie) - na przykład String, Bundle (o którym jeszcze powiemy) lub dowolny serializowalny obiekt (choć wszyscy piszą, żeby unikać zapisywania serializowanych obiektów, że lepiej uczynić klasę takiego obiektu serializowalną, bo tak ponoć wydajniej). Jest też do pary metoda Parcel#readValue, która najpierw wczytuje magiczny int oznaczający rodzaj zapisanej rzeczy, a potem w odpowiedni sposób odczytuje tę rzecz i zwraca Object. Korzystanie z metod writeValue i readValue jest ciut bardziej pamięciożerne od korzystania z metod writeCoś i readCoś (dodaje czterobajtowego inta przed każdą zapisywaną rzeczą), ale lepiej chroni przed próbą odczytania innego typu niżeśmy zapisali - readCoś w takiej sytuacji zwróci śmieci, a readValue zwróci obiekt takiego typu, jaki był naprawdę zapisany, a nie takiego, jaki chcieliśmy odczytać, więc przy pierwszym rzutowaniu z typu Object na spodziewany przez nas typ dostaniemy wyjątek (w czasie wykonania, oczywiście - żaden z tych dwu sposobów nie daje bezpieczeństwa w czasie kompilacji). Ponieważ czasem chcemy zapisać do parceli różnotypowy opis czegoś, a nie chcemy tworzyć klasy, jest klasa Bundle. Ta klasa ma słownik mapujący String na Object i jest parcelowalna, a do tego Parcel umie zapisywać takie bundle w specjalny sposób, ciut wydajniejszy niż zwykłe Parcelable. Bo przy zwykłym parcelablu przed danymi zostaje zapisana nazwa klasy (i, jeśli zapisujemy przez writeValue, magiczny int oznaczający, że teraz idzie Parcelable), a przed bundlem zostaje zapisane nic (i, jeśli zapisujemy przez writeValue, magiczny int oznaczający, że teraz idzie Bundle). Do tego Bundle utworzony z parceli wczytuje się leniwie - przy stworzeniu tylko zapamiętuje sobie, która parcela jest jego źródłem, a dane wczytuje przy pierwszym odczycie. Niestety, nie znam sposobu na odczytanie bajt po bajcie tego, jak jakieś rzeczy są zapisane w parceli. Próbowałem robić to tak: Parcel parcel = Parcel.obtain(); parcel.writeString("ABC"); parcel.setDataPosition(0); String bytes = ""; for (int i=0; i<20; i++) { byte b = parcel.readByte(); bytes += b + " "; } Log.i("test", "bajty: " + bytes); Ale dawało to dziwny wynik, taki: bajty: 3 65 67 0 0 0 Oto, jak działają taski. Żeby mógł działać przycisk "wstecz", musi być określone, czym są te jednostki, między którymi przechodzimy naprzód i wstecz. W WWW są nimi pary (URL, parametry). W Androidzie są nimi intenty. Intent określa do jakiej aktywności chcę przejść (przez podanie nazwy klasy lub bardziej opisowego adresu) oraz jakie przekazuję jej dokładne parametry (w extras intenta). Ciąg przechodzenia od aktywności do aktywności i wstecz to task. Task zaczyna życie, kiedy w launcherze klikam na ikonkę. To kliknięcie powoduje, że jeśli już jest task zaczynający się od tej aktywności (lub innej aktywności z tej aplikacji) (ale zaczynający się, a nie zawierający ją czy posiadający ją na szczycie), to wyświetla mi się szczytowa aktywność z tego taska, a jeśli nie ma takiego taska, to jest tworzony. Task kończy się, kiedy cofając się wrócę do pierwszj aktywności i będąc w niej jeszcze raz nacisnę wstecz. Task jest stosem intentów. W ramach taska uruchamiane są kolejne aktywności. Każda aktywność jest uruchamiana w procesie swojej aplikacji - to znaczy, że aktywności z jednej aplikacji są uruchamiane w tym samym procesie, a z różnych aplikacji - w różnych. Co ma sens, bo dzięki temu działa sensownie mechanizm uprawnień, które są per aplikacja, a realizowane niekiedy (niektóre) przez mechanizm UID-ów. Jeśli przejdę z aktywności o niskich uprawnieniach do aktywności, która ze względu na swój niski UID może robić coś szczególnego, lub po prostu może czytać pliki należące do usera, na którym chodzi ten proces, to ta aktywność nadal będzie mogła to zrobić - mimo że weszliśmy do niej z taska, który zaczął się od aktywności pochodzącej z aplikacji, która takich praw nie miała. Normalnie kiedy przechodzę do kolejnej aktywności, stara nie jest usuwana z pamięci (choć w telefonie, w ustawieniach dewelopera mogę włączyć dla testu takie zachowanie). Więc może się zdarzyć, że w dwu taskach będą dwa intenty będące dwoma różnymi obiektami tej samej klasy żyjącymi w tym samym procesie. Jeśli w każdym z tasków przejdę do kolejnej aktywności - w każdym tasku innej - a potem w każdym z nich się cofnę, to w każdym trafię do właściwego obiektu intenta (jeśli nie był jeszcze zniszczony), bo cofam się nie do aktywności klasy zapamiętanej w tasku, ale do aktywności uruchomionej intentem zapamiętanym w tasku. Kiedy Androidowi brakuje pamięci, może usuwać nieużywane aktywności, może też zabijać procesy nieużywanych aktywności. Ma też prawo niszczyć stare taski, ale je akurat trzyma długo, bo są lekkie. Oto, jak się szuka wycieków pamięci. Robię aktywność, która tworzy obiekt klasy anonimowej i przyczepia go do czegoś, co ma cykl życia dłuższy niż aktywność. Na przykład taką aktywność pokazującą położenie z GPS-a: package pl.psobolewski.wyciek; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.Bundle; import android.app.Activity; import android.view.Menu; import android.widget.TextView; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); LocationManager lm = (LocationManager)getSystemService(LOCATION_SERVICE); lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, new LocationListener() { @Override public void onStatusChanged(String provider, int status, Bundle extras) { } @Override public void onProviderEnabled(String provider) { } @Override public void onProviderDisabled(String provider) { } @Override public void onLocationChanged(Location location) { double lat = location.getLatitude(); double lon = location.getLongitude(); ((TextView) findViewById(R.id.napis)).setText(lat + " " + lon); } }); } } Ona musi mieć w AndroidManifest.xml uprawnienie do GPS-a: (...) Upewniam się, że mam w Eclipse zainstalowaną wtyczkę Memory Analyzer (zwaną też MAT). E... nie wiem, jak sprawdzić, czy jest zainstalowana. W każdym razie jeśli nie jest, to będzie się dało robić zrzuty sterty, ale nie będą się one automatycznie otwierać. Instaluje się ją przez help -> install new software, a URL do update site można znaleźć na tej stronie: http://www.eclipse.org/mat/downloads.php (i brzmi on http://download.eclipse.org/mat/1.3/update-site/). Odpalam z Eclipse'a aplikację na telefonie. Wchodzę w perspektywę DDMS. Mam po lewej listę aplikacji, zaznaczam tę moją. Klikam przycisk dump hprof file (jak wygląda ten przycisk, to sobie wygugluj). Robi się zrzut i otwiera się w eklipsie (bo tak jest ustawione w window -> preferences -> android -> ddms -> hprof action). Obracam telefon kilka razy, żeby aktywność powyciekała. Znowu klikam przycisk dump hprof file - drugi zarzut się otwiera. W nim klikam przycisk "histogram" (przycisk wygląda jak wykres słupkowy), otwiera się lista klas, i ile jest obiektów tej klasy. Klikam przycisk "compare to another heap dump" (pozioma podwójna gruba żółta strzałka), wybieram, z którym wcześniejszym dumpem chcę porównać (z tym pierwszym). Widzę, ile przybyło obiektów jakich klas - większość klas nic mi nie mówi. Na górze listy jest okienko do szukania, wpisuję regekspa z naswą mojego pakietu, żeby znaleźć tylko moje klasy (bo one mi coś powiedzą). Widzę, że przybyło sporo obiektów klasy pl.psobolewski.MainActivity - więc to ona wycieka. Wyłączam compare, wyszukuję na liście klasę pl.psobolewski.MainActivity, klikam prawym, list objects -> with incoming references. Widzę obiekty, po referencjach próbuję dojść, kto trzyma te obiekty (this$0, a jego mListener z klasy LocationManager, oraz this$0, a jego key z hashmapa z mListeners z klasy LocationManager). Oto jak działają fragmenty. Fragmenty to obiekty, które rysują fragment ekranu. Ich cykl życia jest krótszy niż cykl życia aktywności. I jest trochę dziwny - bo czasem ja tworzę fragment przez "new", a czasem to Android tworzy fragment. Jeśli chcę, żeby w ramach danej aktywności był narysowany fragment, muszę najpierw napisać klasę dziedziczącą po klasie android.app.Fragment. Żeby ten fragment coś rysował na ekranie (a zwykle tego chcemy), przykrywamy jego metodę onCreateView. Ta metoda dostaje: - inflejtera, którym możemy nadmuchać widok - widok będący kontenerem, w którym będzie umieszczony nasz widok (bo może zechcemy przekazać go inflejterowi przy dmuchaniu, ze względu na te znane sprawy z obiektem layoutparams) - bundla z zapisanym stanem (ten stan mogę zapisać, dokładnie tak samo jak stan aktywności, przykrywając metodę onSaveInstanceState()) A powinna zwrócić widok. Teraz żeby użyć tego fragmentu w aktywności, mamy dwie możliwości. (... tu nie dokończyłem pisania) Oto, jak działają loopery i okolice: Opis pierwszy: Looper to obiekt, który ma kolejkę zadań. Na tę kolejkę można dodawać zadania, a looper ma metodę "wejdź w pętlę", i wtedy on w nieskończonej pętli bierze komunikaty z kolejki (jak komunikatu nie ma, to się blokuje) i je wykonuje. Więc można - i do tego służy Looper - w jednym wątku stworzyć i uruchomić Loopera, z drugiego wątka wrzucać zadania, a one się będą wykonywać w tym pierwszym wątku. Tylko że obiektu klasy Looper nie tworzymy przez new (bo konstruktor jest prywatny), tylko statyczną metodą, która tworzy obiekt, ale go nie zwraca, tylko go zapamiętuje w ThreadLocal. I potem jak coś chcemy zrobić z looperem - na przykład puścić go w pętlę - to nie wołamy wprost metody na konkretnym obiekcie, tylko wołamy statyczną metodę klasy Looper, ona bierze z ThreadLocal jedyny looper tego wątku i na nim woła co trzeba. Co jest chyba po to, żeby na pewno był tylko jeden looper w jednym wątku? Zresztą, sam nie wiem, po co tak jest. A jak chcemy dodawać zadania do kolejki, to też się to robi dziwnie. Tworzy się obiekt klasy Handler (normalnie, przez new), jego konstruktor znajduje w ThreadLocal loopera i trzyma do niego referencję. I potem jak chcemy dodać komunikat, to używamy do tego metody Handler#post - ona wrzuca dane zadanie na kolejkę (będącą obiektem klasy MessageQueue) loopera aktualnego wątku. A same zadania, co się wrzuca, to nie są gołe Runnable, ale runnable opakowane w obiekt Message. Handler ma też metody, które pozwalają zlecić, żeby dane zadanie było wykonane za określony czas albo o określonej godzinie. Opis drugi: Czasem trzeba coś przesłać z jednego wątka do drugiego. Jakieś dane albo jakiegoś runnable'a. Jest do tego klasa Message - jej obiekty służą do opakowywania danych, któe zechcemy przesyłać. Obiekty tej klasy mają dwa pola typu int (arg1 i arg2) na proste dane, jedno pole typu int na rodzaj komunikatu (what), pole typu Object na dowolne dane i pole typu Runnable na rzecz do wykonania. I jeszcze pole target na obiekt, który ma się zająć obsłużeniem tego komunikatu zawartych w message'u w docelowym wątku. To pole jest typu Handle - a na czym polega, że klasa Handle obsługuje komunikat, to zaraz dokładniej powiem. Do przyjmowania komunikatów służą obiekty klasy Looper. Taki Looper ma kolejkę komunikatów i niekończącą się metodę loop. Metoda loop pobiera kolejne komunikaty z kolejki i dla każdego z nich robi to, że wyciąga z niego target (który, pamiętajmy, jest Handlerem) i każe mu obsłużyć ten komunikat. Więc można w pierwszym wątku stworzyć Loopera i odpalić jego metodę loop, a w drugim wrzucać komunikaty (obiekty Message) na kolejkę Loopera - i w ten sposób przekazywać te komunikaty z wątku drugiego do wątku pierwszego. Tylko tworzenie Loopera i odpalenie jego metody loop robi się nietypowo - nie tworzy się przez new (bo konstruktor jest prywatny), tylko statyczną metodą, która tworzy obiekt, ale go nie zwraca, tylko go zapamiętuje w ThreadLocal. I potem jak coś chcemy zrobić z looperem - na przykład puścić go w pętlę - to nie wołamy wprost metody na konkretnym obiekcie, tylko wołamy statyczną metodę klasy Looper, ona bierze z ThreadLocal jedyny looper tego wątku i na nim woła co trzeba. A w głównym wątku loopera tworzyć nie trzeba, bo w nim już jest looper (który, na przykład, woła na naszej aktywności onStart). Widać więc, że to, co w docelowym wątku zostanie zrobione z komunikatem, zależy od tego handlera, który umieścimy w polu target message'a. Zaraz o tym Handlerze powiem, tylko najpierw zobaczmy, jak się tworzy Message'a i umieszcza w jego polu target odpowiedniego Handlera. Można tworzyć Message przez new - konstruktor jest publiczny - ale dokumentacja zaleca tworzenie przez statyczną metodę obtain. Ta metoda ma pulę gotowych Message'y (domyślnie jednoelementową) i dopóki pula się nie wyczerpie, nie tworzy nowych obiektów, tylko daje z puli. Obiekty klasy Message mają też metodę recycle, która przywraca message'a do puli, a tę metodę woła na message'u looper po tym, jak odpowiedni handler obsłuży message'a. Dzięki temu jeśli robimy message'e przez obtain, to mamy szansę, że nie będą tworzone nowe obiekty. I ta metoda obtain ma wiele przeciążonych wersji, którym się przekazuje, co ma być w polach tego message'e - na przykład można przekazać handlera, który ma być umieszczony w polu target. Ale w praktyce często robi się jeszcze inaczej. Obiekty klasy Handler mają metodę obtainMessage, która tworzy (i zwraca) message'a umieszczając od razu w jego polu target thisa (znaczy, tego handlera, na którym zawołaliśmy obtainMessage). No to teraz wreszcie mogę powiedzieć, co to znaczy, że handler obsługuje message'a. Handler ma metodę dispatchMessage, i to ją wywołuje Looper i przekazuje jej message'a, żeby ta metoda dispatchMessage obsłużyła ten komunikat. Tej metody raczej się (w praktyce) nie przykrywa - ona wywołuje kolejne rzeczy, i to dopiero te kolejne rzeczy ewentualnie przykrywamy, zmieniamy itp. Jeśli message ma w polu callback jakiegoś Runnable'a, metoda dispatchMessage go uruchamia. Przy tworzeniu obiektu klasy Handler można mu przekazać obiekt implementujący prościutki interfejs Handler.Callback - jeśli dany handler ma callbacka, to temu callbackowi (dokładniej, jego metodzie handleMessage) daje message'a do obsłużenia. Poza tym klasa Handler ma pustą, przeznaczoną do przykrywania metodę handleMessage - metoda dispatchMessage uruchamia i ją i przekazuje jej komunikat. Więc żeby zdecydować, co ma się stać w głównym wątku, możemy: - w polu callback umieścić Runnable'a - ale on nie będzie miał dostepu do pól (np. arg1, arg2 i obj) message'a - w polach arg1, arg2 i obj umieścić parametry i zrobić jedno z dwu: - handlerowi przekazać (przy tworzeniu) callback robiący coś z komunikatem - zadziedziczyć po klasie Handler i przykryć metodę handleMessage W praktyce obserwuję taką praktykę, że jeśli ktoś chce przyjmować w wątku dowolne rzeczy do zrobienia, to nie przykrywa handleMessage ani nie tworzy klasy implementującej Handler.Callback, tylko robi komunikaty z Runnablem w polu callback (zresztą jest na to uproszczony sposób, jeszcze go zobaczymy). A jeśli chce uruchamiać w wątku rzeczy te same, a różniące się tylko parametrami, to dziedziczy po Handlerze, przykrywa handleMessage i przekazuje komunikaty z parametrami umieszczonymi w arg1, arg2 lub obj. To jeszcze opowiem, jak się wrzuca message'e na kolejkę loopera. Nie robi się tego przez loopera, a przez handlera. Każdy handler w konstruktorze sprawdza w thread local, czy w jego wątku działa jakiś looper i jeśli tak, zapamiętuje sobie referencję do niego. I jak chcemy umieszczać message na kolejce loopera, to w wątku, w którym działa ten looper, tworzymy handlera, przekazujemy go do drugiego wątku i w tym drugim wątku na handlerze wołamy odpowiednie metody. A metody mamy takie: jedne przyjmujące message'a, inne przyjmujące Runnable'a i pakujące go w message'a. I jedne dostarczające komunikat o określonej godzinie, drugie dostarczające za określony czas, trzecie dostarczające natychmiast. A ponieważ te dwa podziały są niezależne, mamy sześć metod, o nazwach {post,sendMessage}{atTime,delayed,""}. A jest jeszcze jeden, bardziej pośredni sposób na wysłanie komunikatu do loopera. Pamiętajmy, że każdy message ma w polu target handlera, który ma go obsłużyć. No i message ma metodę sendToTarget, która przekazuje ten komunikat metodzie sendMessage handlera tego message'a, a ten handler przekazuje tego message'a swojemu looperowi. Oto, jak działa powiadamianie o tym, że jakieś dane się zmieniły, przez content resolvera. Content resolver ma metodę pozwalającą, żeby każdy, kto chce, mógł powiedzieć, że pewne dane się zmieniły, i każdy, kto chce, mógł się zawczasu zarejestrować, że jak się jakieś dane zmienią, to chce, żeby go powiadomić. Żeby zarejestrować słuchacza, na content resolverze (w aktywności dostaniemy go z metody Context#getContentResolver) wołamy registerContentObserver. Jemu przekazujemy URI, które chcemy obserwować, oraz obiekt anonimowej klasy dziedziczącej po ContentObserver. Konstruktorowi klasy ContentObserver musimy przekazać handlera - jak przyjdzie powiadomienie o zmianie danych, to ten właśnie handler dostanie do uruchomienia nasz callback. Więc jeśli w tym miejscu po prostu damy "new Handler()", to ten handler podłączy się do loopera w bieżącym wątku (o ile bieżący wątek ma loopera), przez co callback wykona się w bieżącym wątku. Więc jeśli robi się to w głównym wątku, to efekt jest, że callback też będzie uruchomiony w bieżącym wątku. Co często jest tym, czego chcemy. No i w tej anonimowej klasie dziedziczącej po ContentObserver możemy przykryć metodę onChange, bo to ona będzie uruchomiona, jak przyjdzie wiadomość, że dane się zmieniły. No a potem żeby wysłać powiadomienie, że dane się zmieniły, na content resolverze (w aktywności dostaniemy go z metody Context#getContentResolver) wołamy notifyChange i przekazujemy URI, pod którym będące dane się zmieniły. Ta komunikacja między kimś, kto zrobił notifyChange, a kimś, kto zrobił registerContentObserver, idzie przez bindera (więc, jeśli dobrze rozumiem, może iść międzyprocesowo) i w ogóle nie tyka (jeśli dobrze rozumiem) content providera, który obsługuje to URI. Ten mechanizm zadziała nawet, jeśli damy URI, którego nie obsługuje żaden content provider. Nawet ten URI nie musi mieć schematu "content://" (choć zwykle ma). Ale jakiś schemat musi mieć - to musi być poprawny URI, bo jak nie, to runtime exception. Oto hello world - najpierw rejestruję listenera, potem wysyłam powiadomienie: protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getContentResolver().registerContentObserver(Uri.parse("kukuryku555"), true, new ContentObserver(new Handler()) { @Override public void onChange(boolean selfChange, Uri uri) { Log.i("@@@", "jest zmiana: " + uri); } }); getContentResolver().notifyChange(Uri.parse("kukuryku555"), null); (...) Czasem bywa tak, że jeden kawałek kodu pobiera dane (w postaci) spod jakiegoś URL-a z content resolvera i dostawszy kursor daje go drugiemu kawałkowi kodu, który z tego kursora korzysta, ale URL-a już nie zna. I ten drugi kawałek kodu (co z danych korzysta, ale URL-a nie zna) potrzebuje być powiadamianym, kiedy dane pod kursorem się zmienią. Ale skoro nie zna URL-a, to jak ma się zarejestrować, że chce obserwować zmiany na nim? Jest to zrobione tak. Po pierwsze, sam z siebie kursor nie wie, z jakiego URL-a pochodzi. Bo w szczególności mogą być kursory nie pochodzące z URL-a. To content provider, który tworzy kursor, zna URL-a (bo content provider tworzy kursor w reakcji na to, że ktoś poprosił o dane z jakiegoś konkretnego URL-a). Więc kursor ma metodę setNotificationUri - ona służy do powiedzenia kursorowi, z jakiego URL-a pochodzi. W normalnej sytuacji ona jest używana tak, że jak content provider tworzy kursor (na przykład przez wydanie zapytania do bazy danych), to przed zwróceniem go woła na nim setNotificationUri i mówi mu, z jakim URL-em jest związany, a kursor to zapamiętuje. I potem każdy może na kursorze zawołać metodę registerContentObserver i przekazać listenera, a wtedy kursor zadziała jako pośrednik - zarejestruje, że chce słuchać zmian na swoim URL-u, a jak dowie się o zmianie, wtedy powiadomi tego listenera. Kiedyś wydawało mi się, że kursor może mieć tylko jednego listenera, że jak się spróbuje zarejestrować drugiego, to pierwszego się nadpisze. Ale okazało się, że się myliłem.