poniedziałek, 15 września 2014

Pojedynek z Androidem - odc.7 Android numer 4 (jeszcze o fragmentach, ActionBar, sieć)

Przyszła pora na kolejny odcinek w stylu chłodnym okiem. Dziś na warsztat wziąłem część funkcjononalności wprowadzonych w Android 4.  Co prawda u bram już puka wersja piąta, ale od czegoś trzeba zacząć. Co dziś mam do skomentowania?

Fragmenty - moim zdaniem przerost infrastrukturalny nad treścią w stosunku do kontrolek XAML czy JavaScript.  Nie będę już wspominać o idei komunikowania się z aktywnością za pomocą interfejsu, aby się nie powtarzać.  Twórcy przewidzieli, że fragmenty mogą się odtwarzać i że powinniśmy zapewnić odtwarzanie ich stanu.  Trudno zrozumieć czemu obrócenie ekranu wszystko zabija i martw tu się developerze, by odtworzyć stan wszystkiego. Każdy ekran to musi być osobna aktywność, do tego stopnia że na wąskich ekranach otwiera się dodatkową aktywność, a na szerokich kontrolki z dwóch aktywności umieszcza się w jednej.  W XAML/HTML obrócenie ekranu nie wpływa na żywotność strony, ot wszystko się dynamicznie przemieszcza i tyle, pomysł z otwieraniem nowej strony na wąskich ekranach smartfonów też czasami się wykorzystuje, ale można też zrobić jedną kontrolkę przełączającą dwa widoki.

ActionBar - dobrze, że powstało coś bardziej funkcjonalnego niż podstawowe menu z dawniejszych czasów. Tak porównując to Windows Phone jako powstały znacznie później od razu wyszedł od idei paska aplikacyjnego i minimum przycisków sprzętowych.  Rodzina Windows/Windows Phone oferuje różne paski w zależności od typu urządzenia. Pomiędzy Windows 8.1 a WP 8.1 kontrolki paska XAML/WinJS z tym samym kodem mogą być już używane.  W Androidzie po prostu jest jeden system na smartfonach i tabletach. Zakładki możemy najbliżej porównać do kontrolek Hub i Pivot (tylko na WP), które wydają mi się bardziej nowoczesne, aczkolwiek nie są częścią paska aplikacyjnego czy nawigacyjnego. Sama obsługa zakładek w Android wydaje mi się niepotrzebnie skomplikowana, od razu listenery, jakieś ręczne sterowanie dodawaniem itd.

Komunikacja sieciowa - obsługa statusu wydaje się dość typowa, obsługa Wi-Fi Direct może nie jest najgorsza, ale przez brak zwykłych eventów nieco skomplikowana choćby przez broadcast receiver i implementacje listenerów. Z kolei obsługa czytania tagów NFC wydaje bardzo prosta, prostsza niż w WP, przy czym powstaje pytanie, jakie jeszcze inne scenariusze z NFC są realizowane przez Androida (w WP mamy także komunikację z innymi urządzeniami oraz płatności). Jak natknę się jeszcze na NFC, to być może zyskam szersze spojrzenie.

 

Android 4.0

  • więcej niż prosta ewolucja
  • platforma obsługująca różne czynniki
    • fragmenty
    • ActionBar
  • właściwości sieciowe zgodne z nowoczesnymi sieciami mobilnymi
    • limity na ilość danych,  kontrola użytkownika
    • komunikacja peer-to-peer pomiędzy urządzeniami
      • Wi-Fi Direct
      • NFC (odczyt tagów)
  • lepszy dostęp do funkcjonalności platformy
    • integracja interfejsu kalendarza z aplikacją
    • uproszczony dostęp do danych kalendarza
      • kalendarze –> zdarzenia –> instancje,  uczestnicy,  przypomnienia

 

Jeszcze o fragmentach

Fragmenty mogą być używane do

  • tworzenia adaptywnego UI
  • zakładek
  • menu kontekstowego
  • modyfikacji action bar

Klasy bazujące na fragmentach

  • Fragment
  • ListFragment
  • DialogFragment
  • PreferenceFragment

Dostawanie aktywności z poziomu fragmentu

  • metoda onAttach wywoływana w momencie podpinania fragmentu do aktywności
  • metoda getActivity

FragmentManager

  • każda aktywność ma własny, getFragmentManager
  • przechwytuje szczegóły tworzenia, pokazywania, ukrywania fragmentów
  • przechowuje referencje do wszystkich fragmentów w aktywności,  findFragmentById

Utrzymywanie stanu fragmentu

  • zmiany w layout często powodują tworzenie nowych instancji fragmentów
  • bieżący stan fragmentu zapisujemy za pomocą onSaveInstanceState
  • zapisany stan jest przekazywany do nowej instancji w metodach onCreate i onCreateView

public class CourseDescriptionFragment extends Fragment  {

         …

         int   _courseIndex;

         …

         @Override

         public View onCreateView(LayoutInflater inflater,  ViewGroup container,  Bundle savedInstanceState)  {

                 …

                 int initialIndex = savedInstanceState == null ? 0 : savedInstanceState.getInt(“courseIndex”, 0);

                 setDisplayedDescription(initialIndex);

                 …

         }

         @Override

         public void onSaveInstanceState(Bundle outState)  {

                  outState.putInt(“courseIndex”,  _courseIndex);

         }

         …

}

Dynamiczne zarządzanie wyświetlaniem fragmentu

  • często zachodzi potrzeba do przełączania między widokami jednej i wielu aktywności
  • FragmentManager pozwala ustalić, które fragmenty są aktualnie wyświetlane, metoda fragmentu isVisible

W zależności od orientacji ekranu możemy wyświetlać jedną aktywność z dwoma fragmentami lub dwie aktywności z jednym fragmentem

public class DynamicActivity extends Activity {

         …

         public void onSelectedCourseChanged (int courseIndex)  {

                   if  (descriptionFragment != null && descriptionFragment.isVisible())

                            //orientacja pozioma - master-details

                            descriptionFragment.setDisplayedDescription(courseIndex);

                   else  {

                            //orientacja pionowa - nowy ekran

                           Intent intent = new Intent(this,  DynamicDetailsActivity.class);

                           intent.putExtra(“courseIndex”,  courseIndex);

                           startActivity(intent);

                   }

         }     

}

 

ActionBar

  • lepiej dostosowany do różnych urządzeń i ekranów
  • także tradycyjne zachowanie menu i obsługa wychodzącego z użycia przycisku sprzętowego ‘menu’
  • różne warianty nawigacji
  • domyślnie aplikacje używają starego zachowania zgodnego z menu
  • dostępny tylko na aktywnościach używających skórek wywodzących się z Theme.Halo
    • automatycznie przy min sdk / target sdk 11 lub nowszym w manifeście
  • ikona na pasku może być klikalna
    • powrót do ekranu home aplikacji
    • przeniesienie użytkownika o jeden poziom wyżej w hierarchii aplikacji
  • opcja podziału na dwa paski (górny i dolny) na wąskim ekranie - w manifeście (w aktywności lub całej aplikacji)
  • zastępuje TabActivity w nawigacji zakładkowej

Potrzeba zarządzania aktywnościami taska przy implementacji home

  • użytkownik powinien mieć wrażenie powrotu
  • nowa aktywność jest domyślnie dodawana na końcu bieżącego stosu
  • przy powrocie “home” wszystkie inne aktywności w stosie powinny zostać wyczyszczone
    • osiągamy za pomocą FLAG_ACTIVITY_CLEAR_TOP

private void onHome()  {

         Intent intent = new Intent(this,  MyActivity.class);

         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);

         startActivity(intent);

}

Nawigacja zakładkowa

  • zakładki są umieszczane bezpośrednio na ActionBar
  • adaptacja do dostępnej przestrzeni na ekranie
    • na wąskich ekranach - pasek z zakładkami pod ikoną aplikacji
    • na szerokich ekranach - zakładki są umieszczone między ikoną aplikacji a opcjami
  • tworzenie
    • wywołujemy setNavigationMode na ActionBar z wartością NAVIGATION_MODE_TABS
    • dla każdej zakładki tworzymy instancję ActionBar.Tab
      • tworzymy Fragment dla zawartości
      • dostarczamy implementację interfejsu ActionBar.TabListener do zarządzania zakładką
        • onTabSelected  - przy pierwszym wywołaniu musi dodać fragment do aktywności, przy sukcesie musi uczynić fragment widocznym
        • onTabUnselected - musi ukryć fragment
        • onTabReselected - naciśnięcie wybranej już zakładki, zwykle nic nie robimy
    • każdą ActionBar.Tab dodajemy do ActionBar
      • poprzez metodę ActionBar.addTab

public class MyTabListener implements ActionBar.TabListener  {

         Fragment _tabFragment;

         boolean _isFirstSelect  = true;

 

         public MyTabListener(Fragment fragment)  {

                 _tabFragment = fragment;

         }

         public void onTabSelected(ActionBar.Tab tab,  FragmentTransaction fragmentTransaction)  {

                  if (_isFirstSelect)  {

                           fragmentTransaction.add(R.id.content,  _tabFragment,  null);

                           _isFirstSelect = false;

                  }

                  else  {

                           fragmentTransaction.attach(_tabFragment);

                  }                 

         }

         public void onTabUnselected(ActionBar.Tab tab,  FragmentTransaction fragmentTransaction)  {

                  fragmentTransaction.detach(_tabFragment);

         }

         public void onTabReselected(ActionBar.Tab tab,  FragmentTransaction fragmentTransaction)  {

         }

}

public class TabNavigatedActivity extends Activity {

         public void onCreate(Bundle savedInstanceState)  {

                 super.onCreate(savedInstanceState);

                 ActionBar actionBar = getActionBar();

                 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

 

                  Fragment tab1Fragment = Fragment.instantiate(this,  Tab1Fragment.class.getName());

                  MyTabListener tab1Listener = new MyTabListener(tab1Fragment);

                  ActionBar.Tab tab1 = actionBar.newTab().setText(“Tab 1”).setTabListener(tab1Listener);

                  …

 

                  actionBar.addTab(tab1);

                  …

         }

}

 

Sieć

Konfigurowalność sieci mobilnej

  • całkowity limit
  • limit dla konkretnej aplikacji
  • blokowanie korzystania z sieci w tle dla danej aplikacji

Ustalanie stanu sieci za pomocą ConnectivityManager

  • Context.getSystemService(CONNECTIVITY_SERVICE);
  • ConnectivityManager.getActiveNetworkInfo
  • zwraca instancję NetworkInfo dla bieżącego połączenia lub null jeśli nie ma w ogóle łączności
  • NetworkInfo.isConnected  - czy można skorzystać z połączenia
  • Nie używać ConnectivityManager.getBackgroundDataSetting, deprecated, zwraca zawsze true

Udostępnienie użytkownikom kontroli nad korzystaniem z sieci przez aplikację

  • udostępnienie preferencji do ograniczeń z korzystania z sieci
  • udostępnienie PreferenceActivity dla strony Android “Data Usage”
    • zdefiniowanie Intent Filter dla akcji MANAGE_NETWORK_USAGE
    • potrzebna kategoria DEFAULT

<activity …>

           <intent-filter>

                     <action android:name=”android.intent.action.MANAGE_NETWORK_USAGE” />

                     <category android:name=”android.intent.category.DEFAULT” />

          </intent-filter>

</activity>

w aktywności lub serwisie

ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);

NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();

HttpGet httpGet = new HttpGet(_url);

HttpResponse httpResponse;

HttpClient httpClient = newHttpClientWithThreadSafeClientConnManager();

InputStreamReader streamReader;

….

httpResponse = httpClient.execute(httpGet);

HttpEntity httpEntity = httpResponse.getEntity();

if  (httpEntity != null && httpEntity.isStreaming()) {

         InputStream inStream = httpEntity.getContent();

         if  (inStream != null)  {

                 streamReader = new InputStreamReader(inStream);

                 while ((bytesRead = streamReader.read(buffer,  0, size)) > 0) {  … }

                 inStream.close();

         }

}

Blokada na łączność w tle nie musi zadziałać na serwis, wykonywanie w tle zależy od tego, czy aktywność jest w tle

Wi-Fi Direct

Wymagane deklaracje w manifeście

<uses-sdk android:minSdkVersion=”14” />   (Android 4.0 lub nowszy)

<uses-feature android:name=”android.hardware.wifi.direct”  android:required=”true” />

<uses-permission android:name=”android.permission.ACCESS_WIFI_STATE” />

<uses-permission android:name=”android.permission.CHANGE_WIFI_STATE”/>

<uses-permission android:name=”android.permission.INTERNET” />

Implementacja

Główny przebieg

  • inicjalizacja systemu
  • inicjalizacja żądań by wykonać specyficzne akcje

Utworzony w runtime broadcast receiver

  • nasłuchiwanie notyfikacji o zakończeniu wykonywanych akcji
  • w niektórych przypadkach wykrywanie sukcesu lub błędu
  • w niektórych przypadkach inicjalizacja następnego kroku wieloetapowej akcji

WiFiP2pManager

  • initialize - przygotowanie API, zwraca WiFiP2pManager.Channel
  • discoverPeers - wyszukanie dostępnych peer-ów
  • requestPeers - pobranie bieżącej listy dostępnych peer-ów
  • connect - ustanowienie połączenia z peer-ami

Wi-Fi Direct informuje aplikacje o istotnych zdarzeniach za pomocą broadcast intents

  • WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION (przy włączaniu/wyłączaniu Wi-Fi Direct,  WiFiP2pManager.EXTRA_WIFI_STATE - bieżący stan)
  • WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION (zmiana informacji o Wi-Fi Direct tego urządzenia)
  • WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION (informuje, że wyniki zapytania o listę dostępnych peer-ów są już dostępne)
  • WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION (informuje, że połączenie zostało ustanowione lub zerwane)

public class MainActivity extends Activity implements WifiP2pManager.ChannelListener {

          private WifiP2pManager _wfdManager;

          private WifiP2pManager.Channel  _wfdChannel;

          private WifiDirectReceiver  _wfdReceiver;

         

          @Override

          public void onCreate(Bundle savedInstanceState)  {

                  …

                  _wfdManager = (WifiP2pManager) getSystemService(WIFI_P2P_SERVICE);

                  _wfdChannel = _wfdManager.initialize(this, getMainLooper(), this);

          }

 

          private void registerWfdReceiver() {

                   _wfdReceiver = new WiFiDirectReceiver(_wfdManager,  _wfdChannel,  this);

                   _wfdReceiver.registerReceiver();

          }

 

          private void unregisterWfdReceiver()  {

                   if (_wfdReceiver != null)

                           _wfdReceiver.unregisterReceiver();

                    _wfdReceiver = null;

          }       

 

          public void onChannelDisconnected()  {

                   _wfdChannel = _wfdManager.initialize(this, getMainLooper(), this);

                   …

          }

         

          public void onClickMenuDiscover(MenuItem item)  {

                    if (isWfdReceiverRegisteredAndFeatureEnabled())  {

                             _wfdManager.discoverPeers(_wfdChannel,  new ActionListenerHandler(this));

                    }

          }

 

          public void onClickMenuConnect(MenuItem item)  {

                   if (isWfdReceiverRegisteredAndFeatureEnabled())  {

                            WifiP2pDevice theDevice = _wfdReceiver.getFirstAvailableDevice();

                            if (theDevice != null)  {

                                    WifiP2pConfig config = new WifiP2pConfig();

                                    config.deviceAddress = theDevice.deviceAddress;

                                    config.wps.setup = WpsInfo.PBC;

                                    _wfdManager.connect(_wfdChannel,  config,  new ActionListenerHandler(this));

                            }

                   }

          }

}

 

public class ActionListenerHandler implements  WifiP2pManager.ActionListener  {

          MainActivity _activity;

          public ActionListenerHandler(MainActivity activity)  {

                  _activity = activity;

          }

          public void onSuccess()  {

          }

          public void onFailure(int i)  {

          }

}

 

public class WiFiDirectReceiver  extends BroadcastReceiver implements

          WifiP2pManager.PeerListListener,  WifiP2pManager.ConnectionInfoListener

{

          …

         public void registerReceiver() {

                   _appMainActivity.registerReceiver(this, getIntentFilter());

         }

         public void unregisterReceiver() {

                  _appMainActivity.unregisterReceiver(this);

         }

         private IntentFilter getIntentFilter() {

                  if (_intentFilter == null) {

                            _intentFilter = new IntentFilter();

                            _intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);

                            _intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);

                            _intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);

                            _intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);

                  }

                  return _intentFilter;

         }

         public void onReceive(Context context, Intent intent)  {

                  String action = intent.getAction();

                  …

                  if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action))  {

                           int wfdState = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE,  -1);

                           /*  WifiP2pManager.WIFI_P2P_STATE_ENABLED ? */

                  }

                  else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action))  {

                            WifiP2pDevice thisDevice = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_DEVICE);

                            …

                  }

                  else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action))  {

                             _wfdManager.requestPeers(_wfdChannel, this);

                  }

                  else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action))  {

                             NetworkInfo info = intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);

                             if (info != null && info.isConnected())  {

                                      _wfdManager.requestConnectionInfo(_wfdChannel);

                             }

                  }

         }

         public void onPeersAvailable(WifiP2pDeviceList wifiP2pDeviceList)  {

                   //warunki

                  _wfdDevices = wifiP2pDeviceList.getDeviceList().toArray(new WifiP2pDevice[0]);

         }

         public void onConnectionInfoAvailable(WifiP2pInfo wifiP2pInfo)  {

                   if  (wifiP2pInfo.groupFormed) {

                            if (wifiP2pInfo.isGroupOwner) {

                                     //otwieramy ServerSocket

                            }

                            else  {

                                     //otwieramy socket do wifiP2pInfo.groupOwnerAddress

                            }

                   }

         }

}

NFC

Android mapuje czytanie tagów NFC na standardowe intencje

  • Kiedy tag zostanie odczytany, system wysyła intent
    • akcja “android.nfc.action.NDEF_DISCOVERED”
    • URI / MIME z taga staje się danymi w intencji
  • Aktywności przechwytują tagi NFC przez utworzenie odpowiedniego filtra intencji
    • powinien zawierać kategorię “DEFAULT”

<activity …>

        <intent-filter>

                 <action android:name=”android.nfc.action.NDEF_DISCOVERED” />

                 <category android:name=”android.intent.category.DEFAULT” />

                 <data android:mimeType=”text/plain” />

        </intent-filter>

</activity>

Brak komentarzy: