Skip to content

2. Bir web hizmeti ile iletişim kuran Android istemcisi şablonu / jSON

Şimdi, bir veya daha fazla web hizmeti ile iletişim kuran bir Android uygulama şablonu sunuyoruz / jSON. Bu, örnekler klasöründeki [architecture] klasöründe bulunan [client-android-skel] projesidir:

  

Bu iskelet uygulamanın incelenmesi, önceki örneklerde karşılaştığımız bazı noktaları tekrar gözden geçirme fırsatı sunacaktır. Bu uygulama, gelecekteki tüm uygulamalar için bir şablon görevi görecektir. Uygulama, sayısız deneme ve düzeltme sürecinin ardından oluşturulmuştur. Yakında geliştireceğimiz uygulamaların mümkün olduğunca çok sayıda öğesini soyut sınıflara ayırmayı amaçlamaktadır; böylece, sadece ayrıntılar açısından farklılık gösteren aynı tür kodları tekrar tekrar yazmak zorunda kalmayız. Özellikleri şunlardır:

  • web sunucusu / jSON ile asenkron iletişim, RxJava kütüphanesi kullanılarak sağlanır;
  • bir parçanın yaşam döngüsü (güncelleme, kaydetme, geri yükleme), belirli zamanlarda alt sınıflarının belirli yöntemlerini çağıran üst sınıfı [AbstractFragment] tarafından yönetilir. Böylece alt sınıf, yaşam döngüsünün aşamalarıyla ilgilenmek zorunda kalmaz, yalnızca üst sınıfı tarafından zorunlu kılınan belirli yöntemleri uygulamakla yetinir;
  • etkinliğin yaşam döngüsü (kaydetme / geri yükleme), yine alt etkinliğe belirli yöntemleri uygulamayı zorunlu kılan [AbstractActivity] adlı soyut sınıf tarafından yönetilir;
  • [AbstractActivity] sınıfı, sekmeli veya sekmesiz, bekleme görüntüsü olan veya olmayan, web sunucusu / jSON üzerinden temel kimlik doğrulaması olan veya olmayan bir uygulamayı yönetebilir. Bu öğelerin varlığı veya yokluğu yapılandırma yoluyla belirlenir;

Bu şablon, bundan sonraki tüm örnekler için kullanılmıştır. Örneklerin çeşitliliği nedeniyle, bir örnekte işe yarayan bir şey bir sonraki örnekte işe yaramayabilirdi. Şablon toplamda yedi örnek için kullanıldığından, çok sayıda yineleme gerçekleştirilmiştir. Bu şablon sekizinci bir örnek için kullanılsaydı, bu yeni örneğin kendine özgü özelliklerinin yine yeni hatalara yol açabileceği muhtemeldir. Bununla birlikte, bu şablonun kullanılması, ilerideki örneklerin yazılmasını önemli ölçüde basitleştirecektir. Nitekim, bir parçanın yaşam döngüsünün yönetimi (güncelleme, kaydetme, geri yükleme) ile parçaların birbirine bitişikliği kavramı birleştiğinde, durum oldukça karmaşık hale gelir. Burada bu süreç, [AbstractFragment] sınıfında tamamen gizlenmiştir.

2.1. Android istemcisinin mimarisi

Önerilen Android istemcisi aşağıdaki mimariye dayanmaktadır:

  • [DAO] katmanı, [IDao] arayüzünü uygular. Web sunucusu / jSON ile iletişim kuran bu katmandır;
  • [IDao] arayüzünü de uygulayan tek bir aktivite vardır. Görünümler, sunucuya ulaşmak için bu aktiviteye başvurur;
  • görünümler, fragmanlar tarafından uygulanır;

Android projesi bu mimariyi yansıtmaktadır:

  

Bu projenin farklı bileşenlerini tek tek tanıtacağız.

2.2. Gradle yapılandırması

 

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Android'in Gradle eklentisi 0.11 sürümünden itibaren, android-apt >= 1.3 sürümünü kullanmanız gerekir
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

apply plugin: 'com.android.application'
apply plugin: 'android-apt'

android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }

  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }

  // APK dosyasını oluşturabilmek için gerekli paketleme seçenekleri
  packagingOptions {
    exclude 'META-INF/ASL2.0'
    exclude 'META-INF/NOTICE'
    exclude 'META-INF/LICENSE'
    exclude 'META-INF/notice.txt'
    exclude 'META-INF/license.txt'
  }
}

def AAVersion = '4.0.0'
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  apt "org.androidannotations:rest-spring:$AAVersion"
  compile "org.androidannotations:rest-spring-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile 'com.android.support:design:23.4.0'
  compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
  compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
  compile 'io.reactivex:rxandroid:1.2.0'
  compile fileTree(include: ['*.jar'], dir: 'libs')
  testCompile 'junit:junit:4.12'
}

repositories {
  maven {
    url 'https://repo.spring.io/libs-milestone'
  }
}
  • Tüm sürüm numaraları değişebilir. Bununla birlikte, Android Studio'yu bu Android araç sürümlerinin (15-16, 47-48. satırlar) mevcut olmasını sağlayacak şekilde yapılandırırsak (bkz. paragraf 6.11), mevcut numaraları kullanabiliriz;

2.3. Uygulama manifesti

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="client.android">

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

  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".activity.MainActivity_"
      android:label="@string/app_name"
      android:windowSoftInputMode="stateHidden"
      android:theme="@style/AppTheme.NoActionBar">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>

</manifest>
  • 3. satır: Uygulamanın paket adını değiştireceğiz;
  • 10. ve 15. satırlar: [app_name] öğesinin değerini [res / values / strings.xml] dosyasında düzelteceğiz. Şu anda bu değer şöyledir:

<?xml version="1.0" encoding="utf-8"?>
<resources>

  <!-- uygulama adı -->
  <string name="app_name">[Donnez un nom à votre application]</string>
</resources>

2.4. Java kodunun yapısı

  
  • [architecture], kodun temel organizasyon öğelerini bir araya getirir;
  • [activity], uygulamanın tek etkinliğini içerir;
  • [fragments], uygulamanın parçalarını veya görünümlerini bir araya getirir;
  • [dao], web sunucusuyla iletişim öğelerini / jSON ile bir araya getirir;

2.5. Faaliyet unsurları

 

Image

2.5.1. Faaliyetle ilişkili görünüm

Etkinliğe ilişkin [activity_main.xml] görünümü şöyledir:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context=".activity.MainActivity">

  <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/appbar_padding_top"
    android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"
      app:layout_scrollFlags="scroll|enterAlways">
    </android.support.v7.widget.Toolbar>
  </android.support.design.widget.AppBarLayout>

  <!-- parça konteyneri -->
  <client.android.architecture.core.MyPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
  • 29. satır: özel bir parça konteyneri kullanılır;

Etkinliğin görünümü için ayrıca bir [res / menu / menu_main.xml] menüsü de bulunmaktadır:


<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity">
</menu>

Şu anda bu menü boştur. Geliştirici, gerekirse bu menüyü dolduracaktır.

2.5.2. [MyPager] parça konteyneri

  

package client.android.architecture;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;

public class MyPager extends ViewPager {

  // kaydırma kontrolü
  private boolean isSwipeEnabled;
  // kaydırma kontrolü
  private boolean isScrollingEnabled;

  // oluşturucular
  public MyPager(Context context) {
    super(context);
  }

  public MyPager(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  // kaydırma işlemini yönetmek için yeniden tanımlanacak yöntemler
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // kaydırma izinli mi?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // kaydırma izinli mi?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }

  // kaydırma kontrolü
  @Override
  public void setCurrentItem(int position){
    super.setCurrentItem(position,isScrollingEnabled);
  }

  // setter'lar
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }

  public void setScrollingEnabled(boolean scrollingEnabled) {
    isScrollingEnabled = scrollingEnabled;
  }
}

Bu sınıf, yalnızca görünümler arasında kaydırma (satır 11) ve kaydırma (satır 13) işlemlerini yönetmek için standart Android sınıfını genişletir.

  • 26-43. satırlar: kaydırma devre dışı bırakılmışsa bunu engelleyen yöntemler;
  • satır 46-49: Görüntülenen görünümü değiştirmek için kullanılan [setCurrentItem] yönteminin yeniden tanımlanması. Kaydırma devre dışı bırakılmışsa, görünüm değişikliği kaydırma olmadan gerçekleşir. Geliştiricinin, istediği kaydırma ayarını belirlemesine olanak tanıyan [setCurrentItem(int position, boolean smoothScrolling)] yöntemini kullanarak bu çalışma şeklini atlayabileceğini unutmayın;

2.5.3. [CoreState] sınıfı

  

[CoreState] sınıfı, çeşitli parçaların durumlarının üst sınıfıdır:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// yapılacaklar: buraya [CoreState]'in alt sınıflarını ekleyin
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // ziyaret edilmiş veya edilmemiş parça
  protected boolean hasBeenVisited = false;
  // parçanın olası menüsünün durumu
  protected MenuItemState[] menuOptionsState;

  // alıcı ve ayarlayıcılar
...
}
  • 16. satır: Her parçanın durumunda, daha önce ziyaret edilip edilmediğini belirten bir boole değeri olan [hasBeenVisited] bulunur. Bu gereklidir çünkü bazen bir parçanın ilk görüntülenişinde yapılması gereken özel işlemler olabilir;
  • satır 18: [client-android-skel] projesi, parçaların menüleri varsa bunları otomatik olarak kaydeder ve geri yükler. MenuItemState[] menuOptionsState tablosunda, menüdeki tüm seçeneklerin görünür olup olmadığı durumu saklanır;
  • 10-13. satırlar: [Exemple-22]'te yapıldığı gibi, etkinliğin ve fragmanlarının durumu oturuma kaydedilecek ve bu oturum da jSON dizesi biçiminde saklanacaktır. Oturumun, [CoreState] türünde bir dizi öğeyi sakladığını göreceğiz. Hiçbir işlem yapılmazsa, [CoreState] türündeki jSON dizesi kaydedilecektir. Oysa biz, [CoreState]'ten türetilmiş durumlar olan parçaların durumlarını kaydetmek istiyoruz. Üst türün dizesi yerine türetilmiş türün jSON dizesinin üretilmesi için, 10-13. satırlarda belirtildiği gibi türetilmiş türleri tanımlamak gerekir. [CoreState] sınıfı, geliştiricinin her yeni uygulama için değiştirmesi gereken mimari sınıflarından biridir (10-13. satırlar);

2.5.4. [IMainActivity] arayüzü

  

[IMainActivity] arayüzü, aşağıdaki mimaride parçaların etkinlikten ne talep edebileceğini belirler:

Image


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // oturuma erişim
  ISession getSession();

  // görünüm değişikliği
  void navigateToView(int position, ISession.Action action);

  // bekleme yönetimi
  void beginWaiting();

  void cancelWaiting();

  // uygulama sabitleri (değiştirilecek) -------------------------------------

  // hata ayıklama modu
  boolean IS_DEBUG_ENABLED = true;

  // sunucu yanıtı için maksimum bekleme süresi
  int TIMEOUT = 1000;

  // istemci isteğinin yürütülmesinden önceki bekleme süresi
  int DELAY = 0;

  // temel kimlik doğrulama
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // parçaların bitişikliği
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // sekme çubuğu
  boolean ARE_TABS_NEEDED = false;

  // bekleme görüntüsü
  boolean IS_WAITING_ICON_NEEDED = false;

  // uygulamanın parça sayısı
  int FRAGMENTS_COUNT = 0;

  // yapılacaklar: sabitlerinizi ve diğer yöntemlerinizi buraya ekleyin
}
  • 6. satır: [IMainActivity] arayüzü, [DAO] katmanındaki [IDao] arayüzünü genişletir;
  • 9. satır: Oturuma erişim sağlayan etkinlik, [ISession] arayüzünün bir örneği biçimindedir;
  • 12. satır: Görünüm değişikliği bu etkinlik aracılığıyla gerçekleştirilir. İkinci parametre, bu görünüm değişikliğini tetikleyen eylemdir; değerler SUBMIT, NAVIGATION veya RESTORE'ten biridir;
  • satır 15-17: Bekleme ekranını yöneten aktivitedir;
  • 22. satır: uygulamanın hata ayıklaması için;
  • satır 25: sunucu yanıt vermezse çok uzun süre beklememek için;
  • satır 28: hata ayıklama sırasında, sunucuyla yapılan işlemi iptal edip neler olduğunu görebilmek için birkaç saniyelik bir değer gireceğiz;
  • satır 31: jSON hizmeti temel kimlik doğrulama isterse, true'e yönlendirilir;
  • satır 34: parçaların bitişikliği;
  • satır 37: Uygulamada sekmeler varsa, vrai olarak ayarlanır;
  • satır 39: Uygulama bir web sunucusuyla iletişim kuruyorsa / jSON ve iletişim sırasında bir bekleme görüntüsü göstermek isteniyorsa vrai;
  • satır 43: uygulama tarafından yönetilen parça sayısı;

[IMainActivity] arayüzü, geliştiricinin tamamlaması gereken mimarinin ikinci öğesidir (satır 45).

2.5.5. [IDao] arayüzü

[IMainActivity] arayüzü, aşağıdaki [IDao] arayüzünü genişletir:

  

package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // web hizmetinin URL'si
  void setUrlServiceWebJson(String url);

  // kullanıcı
  void setUser(String user, String mdp);

  // istemci zaman aşımı
  void setTimeout(int timeout);

  // temel kimlik doğrulama
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // hata ayıklama modu
  void setDebugMode(boolean isDebugEnabled);

  // İstek öncesinde istemcinin bekleme süresi (milisaniye cinsinden)
  void setDelay(int delay);

  // yapılacaklar: arayüzünüzü buraya bildirin
}
  • 24. satır: Geliştirici bu kısmı arayüzü tamamlayacaktır;

2.5.6. Oturum

  

[Session] sınıfı, etkinlik ve parçalar tarafından paylaşılan öğeleri kapsüller. Aşağıdaki [ISession] arayüzünü uygular:


package client.android.architecture.core;

import client.android.architecture.custom.CoreState;

public interface ISession {

  // en son görüntülenen görünümün numarası
  int getPreviousView();

  void setPreviousView(int numView);

  // bir görünümün son durumu
  CoreState getCoreState(int numView);

  void setCoreState(int numView, CoreState coreState);

  // devam eden eylem
  enum Action {
    SUBMIT, NAVIGATION, RESTORE, NONE
  }

  Action getAction();

  void setAction(Action action);

  // tüm görünümlerin durumları -
  // kod tarafından kullanılmaz ancak serileştirme / ters serileştirme için gereklidir jSON
  CoreState[] getCoreStates();

  void setCoreStates(CoreState[] coreStates);

  // en son seçilen sekmenin numarası
  int getPreviousTab();

  void setPreviousTab(int position);

  // sekme seçildiğinde gezinme
  boolean isNavigationOnTabSelectionNeeded();

  void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}

Oturumda belirli yöntemlerin bulunmasını zorunlu kılmak için [ISession] arayüzünü tanıtıyoruz:

  • 7-10. satırlar: en son görüntülenen görünümün (parça) numarası;
  • 12-15. satırlar: belirli bir görünümün durumu;
  • 17-24. satırlar: devam eden eylem kavramını ekliyoruz. Dört tane vardır (17. satır):
    • RESTORE: bir yedekleme / geri yükleme işlemi devam ediyor. Görünümde herhangi bir değişiklik yok;
    • NAVIGATION: bir gezinme işlemi devam ediyor. Burada gezinme, yeni görünümün oturumda son kaydedilen durumundan geri yüklenebildiği bir görünüm değişikliği olarak adlandırılacaktır;
    • SUBMIT: Görünüm değişikliği olduğunda ve yeni görünüm yalnızca kendi durumuna değil, genel olarak etkinliğin durumuna bağlı olduğunda, devam eden eyleme [SUBMIT] türü atanır. Bazen NAVIGATION ile SUBMIT arasında ayrım yapmak zor olabilir. Bu durumda, SUBMIT'in en genel durumu dikkate alınır;
    • NONE: Eylem henüz ilk değerini almamışsa, eylemin değeri;
  • satır 26-30: etkinlik ve parçaların durumları, CoreState[] tipi bir dizide saklanacaktır. Bu dizinin jSON serileştirme/serileştirme kaldırma işlemleri sırasında doğru şekilde yönetilebilmesi için bir getter ve bir setter içermesi gerekir;
  • satır 32-35: en son seçilen sekmenin numarası. Kaydetme/geri yükleme döngüsü sırasında, cihazın döndürülmesinden önce seçili olan sekmeyi yeniden seçmek için kullanılır;
  • satır 37-40: Bir sekmenin seçilmesinin bir parça değişikliğiyle birlikte gerçekleşip gerçekleşmeyeceğini belirten bir boole değerinin yönetimi;

[ISession] arayüzü, aşağıdaki [AbstractSession] soyut sınıfı tarafından uygulanır:


package client.android.architecture.core;

import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import com.fasterxml.jackson.annotation.JsonIgnore;

public class AbstractSession implements ISession {
  // önceki görünümün numarası
  private int preViousView;

  // görünümlerin durumu
  private CoreState[] coreStates = new CoreState[0];

  // devam eden işlem
  private Action action = Action.NONE;

  // önceden seçilen sekme
  private int previousTab;

  // seçilen sekmeye geçiş
  @JsonIgnore
  private boolean navigationOnTabSelectionNeeded = true;

  // oluşturucu
  public AbstractSession() {
    // parça durum tablosu başlatılıyor
    coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
    for (int i = 0; i < coreStates.length; i++) {
      coreStates[i] = new CoreState();
    }
  }


  // ISession arayüzü ---------------------------------------------------------
  @Override
  public int getPreviousView() {
    return preViousView;
  }

  @Override
  public void setPreviousView(int numView) {
    this.preViousView = numView;
  }

  @Override
  public CoreState getCoreState(int numView) {
    return coreStates[numView];
  }

  @Override
  public void setCoreState(int numView, CoreState coreState) {
    coreStates[numView] = coreState;
  }

  @Override
  public Action getAction() {
    return action;
  }

  @Override
  public void setAction(Action action) {
    this.action = action;
  }

  @Override
  public CoreState[] getCoreStates() {
    return coreStates;
  }

  @Override
  public void setCoreStates(CoreState[] coreStates) {
    this.coreStates = coreStates;
  }

  @Override
  public int getPreviousTab() {
    return previousTab;
  }

  @Override
  public void setPreviousTab(int position) {
    this.previousTab = position;
  }

  @Override
  public boolean isNavigationOnTabSelectionNeeded() {
    return navigationOnTabSelectionNeeded;
  }

  @Override
  public void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelectionNeeded) {
    this.navigationOnTabSelectionNeeded = navigationOnTabSelectionNeeded;
  }
}
  • 9. satır: Şu anda görüntülenmekte olan görünümden önce görüntülenen görünümün numarası. Bu bilgi, bir görünüme birden fazla yerden ulaşılabildiğinde yararlıdır. Bu durum, genellikle sekmeli gezinmede geçerlidir. Böylece görüntülenen görünüm, bir önceki görünümün hangisi olduğunu bilebilir;
  • 12. satır: Etkinlik tarafından görüntülenen tüm parçaların durum tablosu;
  • satır 18: Daha önce seçili olan sekmenin numarası. Satır 9'daki önceki görünüm numarasıyla benzer bir işlev görür. Bu bilgi, cihaz döndürüldüğünde ve döndürme öncesinde seçili olan sekmeye geri dönülmesi gerektiğinde yararlıdır;
  • satır 22: Bir sekmenin seçilmesinin, görüntülenen parçanın değişmesiyle birlikte gerçekleşip gerçekleşmeyeceğini belirten bir boole değeri. [client-android-skel] projesinin, sekme sayısının parça sayısından az olduğu durumlarda da kullanılabilmesi için sekmeleri ve parçaları ayrı ayrı yönettiği unutulmamalıdır. İki tür seçim vardır:
    • kullanıcının bir sekmeye tıkladığında yaptığı seçim. Bu durumda, genellikle görüntülenen parça değişmelidir;
    • [Tablayout.Tab.select()] yöntemi aracılığıyla yapılan yazılım seçimi. Bu durumda, görüntülenen parçanın değişmesi her zaman istenmez. İşte iki örnek:
      • cihaz döndürüldüğünde, etkinlik yeniden oluşturulur ve sekmeler de yeniden oluşturulur. Ancak 1. sekme oluşturulduğunda, otomatik olarak [select] yazılım işlemi uygulanır. Bu durumda, görüntülenen parçayı değiştirmek istenmez; çünkü aktivitenin yeniden oluşturulma aşamasındayız ve nihayetinde görüntülenen parça, ilk sekmeyle ilişkili olan parça olmayabilir;
      • sekme yönetimi, fragman yönetiminden ayrı olduğundan, ilişkili fragmanlara müdahale etmeden sekmeleri güncellemek (silme, ekleme) istenebilir. Ancak bu işlemlerin bazıları yine de sekmelerden birinde [select] numaralı örtük bir yazılım işlemini tetikleyebilir. Bu seçim, ilgili parçaya yönlendirme ile sonuçlanmak zorunda değildir;
  • 21. satır: [navigationOnTabSelectionNeeded] alanı, etkinliğin ve parçalarının kaydedilmesi işlemleri sırasında kaydedilmeye yönelik değildir. [@JsonIgnore] açıklaması, jSON serileştirme/serileştirme kaldırma işlemleri sırasında bu alanın göz ardı edilmesini sağlar;
  • 25-31. satırlar: Oluşturucu, uygulamanın [FRAGMENTS_COUNT] parçalarının durum tablosunu başlatır. Bu tablonun elemanları, [hasBeeenVisited=false] alanı ile başlatılır. Bu bilgi, parçanın ilk ziyaret olup olmadığını belirlemek için kullanılır;

[Session] sınıfı şu şekildedir:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // parçalar arasında ve parçalar ile etkinlik arasında paylaşılacak veriler
  // jSON'te serileştirilemeyen öğeler @JsonIgnore ek etiketine sahip olmalıdır
  // jSON formatında serileştirme/deserileştirme için gerekli getter ve setter yöntemlerini unutmayın
}
  • 5. satır: [Session] sınıfı, az önce incelediğimiz [AbstractSession] sınıfını genişletir. Geliştirici, bu sınıfa parçalar arasında ve parçalar ile etkinlik arasında paylaşılacak öğeleri yerleştirecektir. [Session] sınıfının artık AA [@EBean] anotasyonuyla anotlanmadığına dikkat edilmelidir. Bu sınıf artık normal bir sınıfa dönüşmüştür;

2.5.7. [AbstractActivity] soyut sınıfı

  

2.5.7.1. Squelette

[AbstractActivity] sınıfı 300 satırdan fazla bir sınıftır. Bunu adım adım inceleyeceğiz. Temel yapısı şöyledir:


package client.android.architecture;

import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import client.android.R;
import client.android.dao.service.IDao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // katman [DAO]
  private IDao dao;
  // oturum
  protected Session session;

  // parça konteyneri
  protected MyPager mViewPager;
  // araç çubuğu
  private Toolbar toolbar;
  // bekleme görüntüsü
  private ProgressBar loadingPanel;
  // sekme çubuğu
  protected TabLayout tabLayout;

  // parça veya bölüm yöneticisi
  private FragmentPagerAdapter mSectionsPagerAdapter;
  // sınıf adı
  protected String className;
  // eşleştirici jSON
  private ObjectMapper jsonMapper;

  // oluşturucu
  public AbstractActivity() {
    // sınıf adı
    className = getClass().getSimpleName();
    // günlük
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "constructeur");
    }
    // jsonMapper
    jsonMapper = new ObjectMapper();
  }

  // uygulama IMainActivity --------------------------------------------------------------------
  ...

  // yaşam döngüsü - etkinliğin yedeklenmesi / geri yüklenmesi ------------------------------------
  ...

  // bekleme görüntüsü yönetimi ---------------------------------
  ...

  // IDao arayüzü -----------------------------------------------------
  ...

  // parça yöneticisi --------------------------------
  ...

  // alt sınıflar
  protected abstract void onCreateActivity();

  protected abstract IDao getDao();

  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);

  protected abstract void navigateOnTabSelected(int position);

  protected abstract int getFirstView();

}

[AbstractActivity] sınıfı:

  • [IMainActivity] arayüzünü uygular (21. ve 55. satırlar);
  • cihazın döndürülmesi sırasında etkinliğin ve parçalarının kaydedilmesini ve geri yüklenmesini yönetir (satır 58);
  • web sunucusu / jSON ile iletişim sırasında bekleme ekranını yönetir (satır 61);
  • [DAO] katmanının IDao arayüzünü uygular (satır 64);
  • parça yöneticisini uygular (satır 67);
  • alt sınıflarına altı yöntemin bulunmasını zorunlu kılar (satır 71-81);

2.5.7.2. [IMainActivity] arayüzünü uygulamak

[IMainActivity] arayüzünün (bkz. paragraf 2.5.4) uygulaması şu şekildedir:


  // IMainActivity uygulaması --------------------------------------------------------------------
  @Override
  public Session getSession() {
    return session;
  }

  @Override
  public void navigateToView(int position, ISession.Action action) {
    if (IS_DEBUG_ENABLED) {
      Log.d(className, String.format("navigation vers vue %s sur action %s", position, action));
    }
    // yeni parçanın görüntülenmesi
    mViewPager.setCurrentItem(position);
    // bu görünüm değişikliği sırasında devam eden eylemi not eder
    session.setAction(action);
}

2.5.7.3. Etkinliğin ve parçalarının durumunun kaydedilmesi

Etkinliğin ve parçalarının durumu tamamen oturumda bulunur. Dolayısıyla, oturumu kaydetmek gerekir. Burada, [Exemple-22] projesinde (bkz. paragraf 1.23) yapılanları tekrar ele alıyoruz:


  // etkinliğin yedekleme/geri yükleme yönetimi ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // üst öğe
    super.onSaveInstanceState(outState);
    // oturumu bir dize biçiminde yedekleme jSON
    try {
      outState.putString("session", jsonMapper.writeValueAsString(session));
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    // günlük
    if (IS_DEBUG_ENABLED) {
      try {
        Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}

2.5.7.4. Etkinliğin ve parçalarının durumunun geri yüklenmesi

Burada oturumu geri yüklememiz gerekiyor. [Exemple-22]'te gösterildiği gibi yapıyoruz:


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // üst öğe
    super.onCreate(savedInstanceState);
    // günlük
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    // geri yüklenecek bir şey mi var?
    if (savedInstanceState != null) {
      // oturum kurtarma
      try {
        session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
      // günlük
      if (IS_DEBUG_ENABLED) {
        try {
          Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
        } catch (JsonProcessingException e) {
          e.printStackTrace();
        }
      }
    } else {
      // oturum
      session = new Session();
    }
...
  • 10-26. satırlar: 2. satırdaki [Bundle savedInstanceState] parametresi null değilse, oturum geri yüklenir (12-17. satırlar);
  • satır 26-29: 2. satırdaki [Bundle savedInstanceState] parametresinin null olması durumu, etkinliğin ilk başlatılmasına karşılık gelir. Bu durumda boş bir oturum oluşturulur;

2.5.7.5. [DAO] katmanının başlatılması


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // üst öğe
    super.onCreate(savedInstanceState);
    // günlük
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    ...
    // katman [DAO]
    dao = getDao();
    if (dao != null) {
      // katman yapılandırması [DAO]
      setDebugMode(IS_DEBUG_ENABLED);
      setTimeout(TIMEOUT);
      setDelay(DELAY);
      setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
    }
...
  // alt sınıflar
  protected abstract IDao getDao();
....
}
  • 11. satır: [DAO] katmanına ilişkin bir referans, alt faaliyetten (21. satır) istenir;
  • satır 14-17: [DAO] katmanı mevcutsa, [IMainActivity] arayüzünde bulunan bilgilerden yola çıkılarak yapılandırılır;

2.5.7.6. Etkinliğe ilişkin görünümün başlatılması

Etkinliğe ilişkin görünüm, 2.5.1 numaralı paragrafta sunulmuştur:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context=".activity.MainActivity">

  <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/appbar_padding_top"
    android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"
      app:layout_scrollFlags="scroll|enterAlways">
    </android.support.v7.widget.Toolbar>
  </android.support.design.widget.AppBarLayout>

  <!-- parça konteyneri -->
  <client.android.architecture.core.MyPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>

Bu görünüm aşağıdaki kodla başlatılır:


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // üst sınıf
    super.onCreate(savedInstanceState);
    // günlük
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // ilişkili görünüm
    setContentView(R.layout.activity_main);
    // görünüm bileşenleri ---------------------
    // araç çubuğu
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // bekleme görüntüsü?
    if (IS_WAITING_ICON_NEEDED) {
      // bekleme görüntüsü ekleniyor
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding loadingPanel");
      }
      // ProgressBar oluşturuluyor
      loadingPanel = new ProgressBar(this);
      loadingPanel.setVisibility(View.INVISIBLE);
      // ProgressBar'in araç çubuğuna eklenmesi
      toolbar.addView(loadingPanel);
    }
...
  • 11. satır: XML [activity_main] görünümü etkinlikle ilişkilendirilmiştir;
  • 14-15. satırlar: araç çubuğu entegre edilmiştir ve desteklenmektedir;
  • satır 17-27: Bekleme görüntüsünün eklenmesi (isteğe bağlı): [IMainActivity] arayüzünde [IS_WAITING_ICON_NEEDED] boole değeri "true" ise;
  • satır 23: [loadingPanel] alanı tarafından referans verilen [ProgressBar] türündeki bekleme görüntüsünün oluşturulması;
  • 24. satır: Başlangıçta bu resim gizlidir;
  • 26. satır: araç çubuğuna eklenir;

2.5.7.7. Sekme Yönetimi

[IMainActivity] arayüzü bir sekme çubuğu gerektirebilir. Bu sekme çubuğu aşağıdaki şekilde eklenir ve yönetilir:


// sekme çubuğu
  protected TabLayout tabLayout;
...

    // sekme çubuğu mu?
    if (ARE_TABS_NEEDED) {
      // sekme çubuğu ekleniyor
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding tablayout");
      }
      // bir parça görüntülenene kadar seçim üzerine gezinme yok
      session.setNavigationOnTabSelectionNeeded(false);
      // sekme çubuğu oluşturma
      tabLayout = new CustomTabLayout(this);
      tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
      // sekmeler çubuğunun uygulama çubuğuna eklenmesi
      AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
      appBarLayout.addView(tabLayout);
      // sekme çubuğu olay yöneticisi
      tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
          // bir sekme seçildi
          if (IS_DEBUG_ENABLED) {
            Log.d(className, String.format("onTabSelected n° %s, action=%s, tabCount=%s isNavigationOnTabSelectionNeeded=%s",
              tab.getPosition(), session.getAction(), tabLayout.getTabCount(), session.isNavigationOnTabSelectionNeeded()));
          }
          if (session.isNavigationOnTabSelectionNeeded()) {
            // sekmenin konumu
            int position = tab.getPosition();
            // bellek
            session.setPreviousTab(position);
            // ilişkili parça görüntüleniyor mu?
            navigateOnTabSelected(position);
          }
        }

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {

        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {

        }
      });
    }

...
  // alt sınıflar
  protected abstract void navigateOnTabSelected(int position);
...
  • satır 12-48: sekme çubuğunun eklenmesi ve yönetimi;
  • 6. satır: [ARE_TABS_NEEDED] sabiti, [IMainActivity] arayüzünde vrai olarak ayarlanmışsa sekme çubuğu eklenir;
  • satır 12: sekme çubuğunun oluşturulması sırasında, kullanıcı tarafından tetiklenmeyen örtük [Tablayout.Tab.select] işlemleri gerçekleşebilir. Bu hatalı seçimler sırasında herhangi bir gezinme işlemini önlemek için [session.navigationOnTabSelectionNeeded] boole değeri faux olarak ayarlanır. Görüntülenecek parçayı seçmek, [navigateToView] yöntemi ile geliştiricinin sorumluluğundadır. Bu parça görüntülendiğinde [session.navigationOnTabSelectionNeeded] boole değeri vrai olarak yeniden ayarlanacaktır (bkz. AbstractFragment sınıfı);
  • 14. satır: [tabLayout] alanı tarafından referans verilen bir sekme çubuğunun oluşturulması. Daha sonra tekrar ele alacağımız, [CustomTabLayout] adlı özelleştirilmiş bir sekme çubuğu kullanıyoruz;
  • 15. satır: Sekme başlıklarının renklerini belirliyoruz. Bu renkler aşağıdaki [res / color / tab_txt.xml] dosyasında bulunur:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:state_selected="true" android:color="#FFFF00" />
  <item android:state_selected="false" android:color="#FFFFFF" />
</selector>
    • (c) satırı: sekme seçildiğinde sekme başlığının rengi;
    • satır (d): sekme seçili olmadığında sekme başlığının rengi;

Bu dosya elbette düzenlenebilir. Renklerin onaltılık kodlarını örneğin burada bulabilirsiniz.

  • satır 17-18: bu sekme çubuğunun XML [activity_main] görünümündeki uygulama çubuğuna eklenmesi;
  • satır 20-47: sekme çubuğunun olay yöneticisi;
  • satır 22-36: Yalnızca [onTabSelected] olayı yönetilir. Bu olay, yönteme parametre olarak geçirilen [Tab tab] sekmesine yapılan bir tıklamaya veya [TabLayout.Tab.select] yazılım işlemine karşılık gelir;
  • satır 30: seçili sekmenin konumu;
  • satır 32: bu konum oturumda kaydedilir;
  • satır 34: şimdi bu sekmeyle ilişkili parçayı görüntüleme aşamasındayız. Yalnızca alt sınıf (satır 52) bu ilişkilendirmeyi yapabilir. Bazı incelenen örneklerde yapıldığı gibi, sekme çubuğunu [mViewPager] parça konteyneriyle ilişkilendirmediğimize dikkat edilmelidir. Burada, sekme çubuğunun yönetimi ile parçaların yönetimi tamamen birbirinden ayrılmıştır. Bu nedenle, bir sekmeye tıklandığında hangi görünümün görüntülenmesini istediğimizi belirtmek zorundayız;
  • 28. satır: gezinme içeren ve içermeyen sekme seçimleri birbirinden ayrılır. Genel olarak, kullanıcı bir sekmeye tıkladığında gezinme istenir, ancak yazılım tarafından yapılan bir seçimde gezinme istenmez. Bu iki durumu ayırt eden, [session.navigationOnTabSelectionNeeded] öğesini kullanan geliştiricidir. Gezinme yapılmadığında, en son seçilen sekmenin numarası oturumda kaydedilmez. Bunu yapmak geliştiricinin sorumluluğundadır;

2.5.7.8. [CustomTabLayout] sekme yöneticisi

  

Sekme başlıklarını farklı yazı tipleriyle görüntüleyebilmek için özelleştirilmiş bir sekme yöneticisi kullanıyoruz. [CustomTabLayout] sınıfı şu şekildedir:


package client.android.architecture.custom;

import android.content.Context;
import android.graphics.Typeface;
import android.support.design.widget.TabLayout;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class CustomTabLayout extends TabLayout {
  private Typeface mTypeface;

  public CustomTabLayout(Context context) {
    super(context);
    init();
  }

  public CustomTabLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
  }

  public CustomTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
  }

  private void init() {
    mTypeface = Typeface.createFromAsset(getContext().getAssets(), "fonts/Roboto-Bold.ttf");
  }

  @Override
  public void addTab(Tab tab) {
    super.addTab(tab);

    ViewGroup mainView = (ViewGroup) getChildAt(0);
    ViewGroup tabView = (ViewGroup) mainView.getChildAt(tab.getPosition());

    int tabChildCount = tabView.getChildCount();
    for (int i = 0; i < tabChildCount; i++) {
      View tabViewChild = tabView.getChildAt(i);
      if (tabViewChild instanceof TextView) {
        ((TextView) tabViewChild).setTypeface(mTypeface, Typeface.NORMAL);
      }
    }
  }

}
  • Sekme başlıklarının yazı tipi özelleştirmesi 30. ve 44. satırlarda yapılır;

[fonts] dosyası şu şekildedir:

  

Kaynaklar:

  • [CustomTabLayout] sınıf koduna URL ve [http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout] dosyalarında rastlandı;
  • yazı tipleri URL ve [https://www.fontsquirrel.com/fonts/roboto]'te bulunmuştur;

2.5.7.9. Son başlatmalar


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // üst sınıf
    super.onCreate(savedInstanceState);
    // günlük
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // parça yöneticisinin örneklenmesi
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
    // parça konteyneri, parça yöneticisiyle ilişkilendirilmiştir
    // yani, parça konteynerindeki i numaralı parça, parça yöneticisi tarafından sağlanan i numaralı parçadır
    mViewPager = (MyPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // fragmanlar arasında kaydırma engellenir
    mViewPager.setSwipeEnabled(false);
    // parçaların bitişikliği
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
    // 1. görünüm görüntülenir
    if (session.getAction() == ISession.Action.NONE) {
      navigateToView(getFirstView(), ISession.Action.NONE);
    }
    // kontrolü alt etkinliğe devredilir
    onCreateActivity();
  }
...
  // alt sınıflar
  protected abstract void onCreateActivity();
  protected abstract int getFirstView();
...
  • 10-19. satırlar: burada, incelenen örneklerde sıklıkla rastlanan kod yer almaktadır;
  • satır 21-23: ilk görünümün görüntülenmesi. Bu durumu ayırt etmenin şüphesiz birkaç yolu vardır. Burada, ilk görünüm için görünüm değişikliğine neden olan eylemin değerinin NONE olması gerçeğinden yararlandık;
  • 22. satır: Görüntülenecek ilk parça hakkında herhangi bir varsayımda bulunmuyoruz. Örneklerimizde bu genellikle 0 numaralı parça olmuştur, ancak her zaman böyle değildir (bkz. Örnek-22). Bu nedenle, alt aktiviteden (30. satır) bu ilk görünümün hangisi olduğunu bize bildirmesini isteyeceğiz;
  • 25. satır: Burada mümkün olan her şeyi faktörlere ayırdık. Artık alt sınıfın kendi başlatma işlemlerini yapması gerekiyor (29. satır);

2.5.7.10. Bekleme görüntüsünün yönetimi

[AbstractActivity] sınıfında, bekleme görüntüsü aşağıdaki iki yöntemle yönetilir:


  // bekleme görüntüsünün yönetimi ---------------------------------
  public void cancelWaiting() {
    if (loadingPanel != null) {
      loadingPanel.setVisibility(View.INVISIBLE);
    }
  }

  public void beginWaiting() {
    if (loadingPanel != null) {
      loadingPanel.setVisibility(View.VISIBLE);
    }
}

2.5.7.11. [IDao] arayüzünün uygulanması

[AbstractActivity] sınıfında, [IDao] arayüzü (bkz. paragraf 2.5.5) şu şekilde uygulanır:


public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // [DAO] katmanı
  private IDao dao;
...
  // IDao arayüzü -----------------------------------------------------
  @Override
  public void setUrlServiceWebJson(String url) {
    dao.setUrlServiceWebJson(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    dao.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    dao.setTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    dao.setBasicAuthentification(isBasicAuthentificationNeeded);
  }

  @Override
  public void setDebugMode(boolean isDebugEnabled) {
    dao.setDebugMode(isDebugEnabled);
  }

  @Override
  public void setDelay(int delay) {
    dao.setDelay(delay);
}
  • 3. satır: Bu alanın değerinin, [onCreate] yönteminde alt etkinlik tarafından sağlandığını hatırlatırız;

2.5.7.12. Parça Yöneticisinin Uygulanması

[AbstractActivity] sınıfında, parça yöneticisi şu şekilde uygulanmaktadır:


...
  // parça yöneticisi --------------------------------
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    private AbstractFragment[] fragments;

    // oluşturucu
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
      // alt sınıf parçaları
      fragments = getFragments();
    }

    // konum numaralı parçayı oluşturmalıdır
    @Override
    public AbstractFragment getItem(int position) {
      // parça görüntülenir
      return fragments[position];
    }

    // yönetilecek parça sayısını gösterir
    @Override
    public int getCount() {
      return fragments.length;
    }

    // konum numaralı parçanın başlığını döndürür
    @Override
    public CharSequence getPageTitle(int position) {
      return getFragmentTitle(position);
    }
  }

  // alt sınıflar
  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);
...
}
  • 5. satır: faaliyete ilişkin parça dizisi. Tüm parçalar [AbstractFragment] sınıfından türetilecektir;
  • 8-12. satırlar: Parça dizisini başlatma işlemini yapıcı gerçekleştirir. Yapıcı, bu parçaları etkinliğin alt sınıfından ister (35. satır);
  • 28-31. satırlar: Parça başlıkları, parça sayısı kadar sekme bulunan bir uygulamada kullanılabilir. Bu durumda, sekmeye parçanın başlığı verilebilir. Burada bu başlıklar alt sınıftan istenir (37. satır);

2.5.7.13. [onResume] yöntemi

[onResume] yöntemi, etkinlikle ilişkili görünüm görünür hale gelmeden biraz önce yürütülür. Burada, kaydetme/geri yükleme işleminden sonra bir sekme seçmek için kullanılır:


  @Override
  public void onResume() {
    // üst öğe
    super.onResume();
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onResume");
    }
    // geri yükleme yapılıyorsa, en son seçilen sekme geri yüklenmelidir
    if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
      tabLayout.getTabAt(session.getPreviousTab()).select();
    }
}
  • 10. satır: kaydetme/geri yükleme işleminden önce seçili olan sekmenin seçilmesi. Burada, aktivitenin yaşam döngüsünde [onResume] yönteminden önce yürütülen [onCreate] yönteminde, sekme seçildiğinde gezinme işleminin engellendiğini unutmamak gerekir. Dolayısıyla burada bir sekme seçiliyor ancak parça değişimi gerçekleşmiyor;

2.5.7.14. Résumé

[AbstractActivity] soyut sınıfı, uygulamanın tek aktivitesinin üst sınıfı olacaktır.

Alt etkinlik, aşağıdaki altı yöntemi uygulamalıdır:


  // alt sınıflar
  protected abstract void onCreateActivity();

  protected abstract IDao getDao();

  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);

  protected abstract void navigateOnTabSelected(int position);

protected abstract int getFirstView();

Ayrıca, alt aktivite, üst sınıfının aşağıdaki korumalı öğelerine erişebilir:


  // oturum
  protected ISession session;
  // parça konteyneri
  protected MyPager mViewPager;
  // sekme çubuğu
  protected CustomTabLayout tabLayout;
  // sınıf adı
protected String className;

2.5.8. [MainActivity] etkinliği

  

[MainActivity] sınıfı farklı bir adla çağrılabilir. Tek kısıtlaması, [IMainActivity] arayüzünü uygulamaktır. Temel olarak sağlanan sınıf şöyledir:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.AbstractActivity;
import client.android.architecture.AbstractFragment;
import client.android.architecture.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // katman [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // oturum
  private Session session;

  // üst sınıf yöntemleri -----------------------
  @Override
  protected void onCreateActivity() {
    // günlük
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // oturum
    this.session = (Session) super.session;
    // yapılacaklar: üst sınıf tarafından başlatılan başlatma işlemlerine devam edelim
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // yapılacaklar: parçaları burada tanımlayın
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // yapılacaklar: parçaların başlıklarını burada tanımlayın
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // yapılacaklar: sekmeli gezinme - görüntülenecek görünümü tanımla
  }

  @Override
  protected int getFirstView() {
    // yapılacaklar: sekmeli gezinme - görüntülenecek ilk görünümü tanımlayın
    return 0;
  }
}
  • 14. satır: 19. satırdaki AA [@Bean] notasyonunun anlaşılabilmesi için, etkinliğin AA [@EActivity] notasyonuna sahip olması gerekir;
  • 15. satır: Etkinlik, XML [menu_main] menüsüyle ilişkilendirilmiştir. Şu anda bu menü boştur. Geliştirici, gerekirse bu menüyü doldurmalıdır;
  • 16. satır: Sınıf, [AbstractActivity] sınıfını genişletir;
  • satır 19-20: [DAO] katmanına bir referans. Bu referans, bu alan başlatılmadan önce AA kütüphanesi tarafından örneklenecektir. Bu, AA ve [Dao] bean'lerinin mevcut olması gerektiği anlamına gelir. Teslim ettiğimiz iskelet uygulamada durum her zaman böyledir. [DAO] katmanının bulunmadığı bir uygulamada bile [dao] paketinin var olmasına izin verilebilir. Bu, herhangi bir soruna yol açmaz;
  • 22. satır: [Session] türünün bir örneği olarak session. Session, üst sınıf [AbstractActivity]'te mevcuttur, ancak [ISession] arayüzünün bir örneği olarak (32. satır);
  • 24-63. satırlar: üst sınıf [AbstractActivity] tarafından zorunlu kılınan altı yöntem;
  • satır 36-39: [getDao] yöntemi, [DAO] katmanına bir referans döndürür. Burada bu referans hiçbir zaman null değildir. Oysa üst sınıf [AbstractActivity]'te, alt sınıfın null referansı döndürmesi durumunda, [DAO] katmanının olmadığını belirtmek üzere bir düzenleme yapılmıştır. Bu olasılıktan yararlanmak istenirse (bence pek kullanışlı değil), null işaretçisini burada döndürmek gerekir;

2.6. [DAO] katmanı

Image

  

2.6.1. IDao arayüzü

Bu arayüz 2.5.5. paragrafında tanıtılmıştır:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // Web hizmetinin URL'si
  void setUrlServiceWebJson(String url);

  // kullanıcı
  void setUser(String user, String mdp);

  // istemci zaman aşımı
  void setTimeout(int timeout);

  // temel kimlik doğrulama
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // hata ayıklama modu
  void setDebugMode(boolean isDebugEnabled);

  // istek öncesi istemci bekleme süresi (milisaniye cinsinden)
  void setDelay(int delay);

  // yapılacaklar: arayüzünüzü buraya tanımlayın
}

Geliştirici, 24. satırdan itibaren [DAO] katmanına ait yöntemleri ekleyecektir.

2.6.2. [WebClient] arayüzü

  

[WebClient] arayüzü şu şekildedir:


package client.android.dao.service;

import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // yapılacaklar: ulaşılması gereken URL'leri burada tanımlayın
}

Geliştirici, 17. satırdan itibaren jSON sunucusu tarafından sunulan URL ile iletişim kuran yöntemleri ekleyecektir.

2.6.3. [MyAuthInterceptor] kimlik doğrulama yakalayıcısı

  

[MyAuthInterceptor] sınıfı şu şekildedir:


package client.android.dao.service;

import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.IOException;

@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {

  // kullanıcı
  private String user;
  // şifre
  private String mdp;

  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    // ele geçirilen HTTP isteğinin HTTP başlıkları
    HttpHeaders headers = request.getHeaders();
    // HTTP temel kimlik doğrulama başlığı
    HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
    // HTTP başlıklarına ekleme
    headers.setAuthorization(auth);
    // HTTP isteğinin yaşam döngüsüne devam ediliyor
    return execution.execute(request, body);
  }

  // kimlik doğrulama öğeleri
  public void setUser(String user, String mdp) {
    this.user = user;
    this.mdp = mdp;
  }
}

Bu sınıf, aşağıdaki HTTP kimlik doğrulama başlığını oluşturur:

Authorization: Basic code

burada [code], 'user:mp' dizesinin Base64 kodudur. Bu sınıf, yalnızca jSON sunucusu bu kimlik doğrulama biçimini bekliyorsa kullanılır. Başka sınıflar da mevcuttur.

Not: Bu sınıfın kullanımı 3.6.3.1. paragrafında gösterilmiştir.

2.6.4. [AbstractDao] sınıfı

  

[AbstractDao] sınıfı şöyledir:


package client.android.dao.service;

import android.util.Log;
import client.android.architecture.core.Utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;

public abstract class AbstractDao {

  // jSON eşleştiricisi
  private ObjectMapper mapper = new ObjectMapper();
  // hata ayıklama modu
  protected boolean isDebugEnabled;
  // sınıf adı
  protected String className;
  // isteğin yürütülmesinden önce bekleme süresi
  private int delay;

  // oluşturucu
  public AbstractDao() {
    // sınıf adı
    className = getClass().getName();
    Log.d("AbstractDao", String.format("constructeur, thread=%s", Thread.currentThread().getName()));
  }

  // korumalı yöntemler ----------------------------------------------------------
  // genel arayüz
  protected interface IRequest<T> {
    T getResponse();
  }

  // bir web hizmetine yönelik genel istek / jSON
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // günlük
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("delay=%s", delay));
    }
    // hizmet yürütme - tek bir yanıt bekleniyor
    return Observable.create(new Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // hizmet yürütme
        try {
          // bekleme?
          if (delay > 0) {
            Thread.sleep(delay);
          }
          // senkron istek yürütülüyor
          T response = request.getResponse();
          // günlük
          if (isDebugEnabled) {
            String log;
            if (response instanceof String) {
              log = (String) response;
            } else {
              log = mapper.writeValueAsString(response);
            }
            Log.d(className, String.format("response=%s sur thread [%s]", log, Thread.currentThread().getName()));
          }
          // yanıt gözlemciye gönderiliyor
          subscriber.onNext(response);
          // gözlemlenebilirin sonu bildiriliyor
          subscriber.onCompleted();
        } catch (InterruptedException | JsonProcessingException | RuntimeException e) {
          // günlük
          if (isDebugEnabled) {
            try {
              Log.d(className, String.format("Thread [%s], Exception communication avec serveur : %s", Thread.currentThread().getName(), mapper.writeValueAsString(Utils.getMessagesFromException(e))));
            } catch (JsonProcessingException e1) {
              Log.d(className, String.format("Erreur jSON imprévue"));
            }
          }
          // bir istisna atılır
          subscriber.onError(new DaoException(e, 100));
        }
      }
    });
  }

  // hata ayıklama modu
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
  }

  public void setDelay(int delay) {
    this.delay = delay;
  }
}
  • 35-81. satırlar: [getResponse] yöntemi, [Observable<T>] türünü oluşturmak için RxAndroid kütüphanesini kullanır. Daha önce görülen bazı örneklerin aksine, burada özel bir tür olan [Response<T>] türü değil, herhangi bir T türü işlenmektedir;
  • 35. satır: [getResponse] yöntemi, 30-32. satırlardaki [IRequest<T>] türünden bir örneği parametre olarak alır; bu örneğin [IRequest.getReponse()] yöntemi, senkron bir HTTP işlemiyle T türünü elde eder;
  • 48-50. satırlar: Yapay olarak [delay] milisaniye beklenir. Üretim ortamında bu değer [delay=0] olarak ayarlanacaktır. Hata ayıklama aşamasında, kullanıcının asenkron işlemi iptal etme ve böylece kodun bu durumda nasıl davrandığını görme şansı olması için [delay=qqs secondes] değeri kullanılacaktır;
  • 52. satır: Beklenen yanıt, senkron bir istekle talep edilir;
  • 64. satır: Yanıt alındığında, gözlemciye aktarılır;
  • 66. satır: Artık herhangi bir veri gönderilmeyeceği belirtilir. Burada, yalnızca bir öğe döndüren asenkron bir eylemin özel bir durumu söz konusudur;
  • 67-78. satırlar: istisna durumunda, istisna gözlemciye iletilir (77. satır);

2.6.5. [Dao] sınıfı

  

[Dao] sınıfı şu şekildedir:


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // web hizmeti istemcisi
  @RestService
  protected WebClient webClient;
  // güvenlik
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // RestTemplate
  private RestTemplate restTemplate;
  // RestTemplate fabrikası
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // günlük
    Log.d(className, "afterInject");
    // restTemplate üretiliyor
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // jSON dönüştürücüsünü sabitliyoruz
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // web istemcisinin restTemplate'i belirlenir
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // web hizmetinin URL'i belirlenir
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // kullanıcı, yakalayıcıya kaydedilir
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // fabrika yapılandırması
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // kimlik doğrulama engelleyicisi mi?
    if (isBasicAuthentificationNeeded) {
      // kimlik doğrulama engelleyicisi ekleniyor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // özel yöntemler -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // yapılacaklar: IDao'in uygulanması
}
  • 21-22. satırlar: web sunucusu / jSON ile veri alışverişini sağlayacak olan AA [WebClient] bean'inin enjeksiyonu;
  • 24-25. satırlar: kimlik doğrulama engelleyicisinin enjeksiyonu;
  • satır 31-42: satır 21-25'teki alanların eklenmesinden sonra yürütülen yöntem;
  • satır 37: İstemci / sunucu arasındaki iletişimi sağlayan [RestTemplate] nesnesi, bir factory nesnesinden oluşturulur. Bu zorunlu değildir, ancak iletişim zaman aşımları factory nesnesi aracılığıyla yapılandırılabilir. Bu nedenle, parametresiz [RestTemplate()] oluşturucusunu kullanmıyoruz;
  • 39. satır: [RestTemplate] dönüştürücülerine bir jSON dönüştürücüsü eklenir. Bu, tek dönüştürücü olacaktır. Ayrıca, [WebClient] istemcisinin bir yöntemi sunucudan bir jSON dizesi aldığında, bu dizi otomatik olarak yöntemin döndürmesi gereken nesneye deserialize edilecektir;
  • 41. satır: Bu şekilde yapılandırılan [RestTemplate] nesnesi, web istemcisine aktarılır; web istemcisi bu nesne sayesinde istemci/sunucu arasındaki veri alışverişini sağlar;
  • 44-48. satırlar: Web sunucusunun kök URL / jSON nesnesi belirlenir. [WebClient] sınıfında tanımlanan tüm URL nesneleri, bu kök URL nesnesine bağlı URL nesneleridir;
  • 50-54. satırlar: Bu yöntem, bağlantı temel tipte bir yetkilendirmeyle kontrol edildiğinde (bkz. 2.6.3. paragraf) bağlantının sahibini belirtmeye olanak tanır;
  • satır 56-64: İstemci/sunucu iletişiminin timeouts değerlerini belirler. Bu, iletişimi yöneten [RestTemplate] nesnesinin factory özelliği aracılığıyla yapılır;
  • 66-78. satırlar: Bu yöntem, sunucunun temel kimlik doğrulama ile korunan bir sunucu olduğunu belirtmeye yarar;
  • 72-77. satırlar: Temel kimlik doğrulama istenirse, 25. satırda eklenen kimlik doğrulama ara alıcısı, [RestTemplate] nesnesinin ara alıcılarına eklenir. Bu ara alıcı, web istemcisinden gelen tüm isteklerin başına, sunucu tarafından beklenen HTTP temel kimlik doğrulama satırını otomatik olarak ekleyecektir;
  • geliştirici, 87. satırdan itibaren [IDao] arayüzünü uygulayacaktır;

2.7. Parçalar

  

2.7.1. [MenuItemState] sınıfı

[MenuItemState] sınıfı, bir menü seçeneğinin durumunu kapsüller:


package client.android.architecture;

public class MenuItemState {

  // menü seçeneğinin kimliği
  private int menuItemId;
  // seçeneğin görünürlüğü
  private boolean isVisible;

  // oluşturucular
  public MenuItemState() {

  }

  public MenuItemState(int menuItemId, boolean isVisible) {
    this.menuItemId = menuItemId;
    this.isVisible = isVisible;
  }

  // alıcı ve ayarlayıcılar
...
}

2.7.2. [Utils] sınıfı

[Utils] sınıfı, yardımcı statik yöntemleri bir araya getirir:


package client.android.architecture;

import java.util.ArrayList;
import java.util.List;

public class Utils {

  // bir istisnanın hata mesajları listesi - sürüm 1
  static public List<String> getMessagesFromException(Throwable ex) {
    // istisna yığınındaki hata mesajlarından bir liste oluşturulur
    List<String> messages = new ArrayList<>();
    Throwable th = ex;
    while (th != null) {
      messages.add(th.getMessage());
      th = th.getCause();
    }
    return messages;
  }

  // bir istisnanın mesaj listesi - sürüm 2
  static public String getMessageForAlert(Throwable th) {
    // görüntülenecek metin oluşturulur
    StringBuilder texte = new StringBuilder();
    List<String> messages = getMessagesFromException(th);
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // sonuç
    return texte.toString();
  }

  // bir istisnaya ait mesaj listesi - sürüm 3
  static public String getMessageForAlert(List<String> messages) {
    // görüntülenecek metin oluşturuluyor
    StringBuilder texte = new StringBuilder();
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // sonuç
    return texte.toString();
  }
}

2.7.3. Üst sınıf [AbstractFragment]

[AbstractFragment] sınıfı, uygulamanın tüm parçalarında ortak olan unsurları bir araya getirir. [AbstractActivity] sınıfında olduğu gibi, bu sınıfın kodu da karmaşıktır. Burada da kodu adım adım inceleyeceğiz.

2.7.3.1. Ana yapı


package client.android.architecture.core;

import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;

import java.util.ArrayList;
import java.util.List;

public abstract class AbstractFragment extends Fragment {

  // özel veriler ------------------------------------------------------------
  // gözlemlenebilir öğelere abonelikler
  private List<Subscription> abonnements = new ArrayList<>();
  // parça menüsü
  private Menu menu;
  private MenuItemState[] menuOptionsStates = new MenuItemState[0];
  // parça yaşam döngüsü
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // parça durumu
  private CoreState previousState;
  // jSON eşleştiricisi
  private ObjectMapper jsonMapper = new ObjectMapper();
  // parça yaşam döngüsü
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
  // asenkron görevler
  private boolean runningTasksHaveBeenCanceled;

  // alt sınıflara erişilebilir veriler ---------------------------------------
  // hata ayıklama modu
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // sınıf adı
  protected String className;
  // asenkron görevler
  protected int numberOfRunningTasks;
  // etkinlik
  protected IMainActivity mainActivity;
  protected Activity activity;
  // oturum
  protected Session session;


  // Fragment güncelleme ----------------------------------------------------------------------------------
 ...

  // menü yönetimi ------------------------------------------
  ...

  // bekleme yönetimi -------------------------------------------------------------
...

  // asenkron işlem yönetimi --------------------------------------------------------------------
...

  // istisna yönetimi -------------------------------------------------------------------
....

  // parça yaşam döngüsü yönetimi --------------------------------------------------------
...

  // alt sınıflar -----------------------------------------------------
  public abstract CoreState saveFragment();

  protected abstract int getNumView();

  protected abstract void initFragment(CoreState previousState);

  protected abstract void initView(CoreState previousState);

  protected abstract void updateOnSubmit(CoreState previousState);

  protected abstract void updateOnRestore(CoreState previousState);

  protected abstract void notifyEndOfUpdates();

  protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);

}
  • 28-45. satırlar: sınıfın özel verileri;
  • 47-58. satırlar: alt sınıflar tarafından erişilebilen korumalı veriler;
  • 61-62. satırlar: görüntülenecek parçayı güncelleyen kod;
  • 64-65. satırlar: olası menüyü yönetmek için yardımcı kod;
  • satır 67-68: asenkron işlem sırasında beklemeyi yönetmek için yardımcı kod;
  • satır 70-71: parçanın [DAO] katmanıyla iletişimini kolaylaştıran kod;
  • satır 73-74: tüm istisnaları standart bir şekilde yönetmek için yardımcı kod;
  • satır 76-77: parçanın yaşam döngüsünü yöneten kod;
  • satır 80-94: üst sınıf, alt sınıflarına 8 yöntem dayatır;

2.7.3.2. Oluşturucu

Sınıfın oluşturucusu şöyledir:


  // sınıf adı
  protected String className;
  // parça yaşam döngüsü
  private boolean fragmentHasToBeInitialized = false;
...
  // oluşturucu ----------------------
  public AbstractFragment() {
    // başlatma
    className = getClass().getSimpleName();
    fragmentHasToBeInitialized = true;
    // günlük
    if (isDebugEnabled) {
      Log.d(className, "constructeur");
    }
}
  • 9. satır: Burada örneklendirilen alt sınıfın adı belirtilir. Bu ad, üst sınıfın tüm günlüklerinde kullanılır;
  • 10. satır: Parçanın oluşturulmakta olduğu belirtilir. Bu bilgi, alt parçadan güncellenmesi istendiğinde kullanılacaktır;

2.7.3.3. Menü yönetimi

Mimari yapımızda, her fragmanın bir menüsü olmalıdır; bu menü boş olsa bile. Günlük kayıtları, fragmanın bir menüsü olduğunda yürütülen [onCreateOptionsMenu] yönteminin çalıştırılması sırasında, fragmanın zaten etkinliği, görünümü ve menüsüyle ilişkilendirilmiş olduğunu ve görünür hale geleceğini göstermiştir. Dolayısıyla bu, görsel arayüzün ve menünün güncellenebileceği bir andır. [onCreateOptionsMenu] yönteminde, alt parçaya kendini güncellemesini talep ediyoruz.

Menü yönetimi, alt parçanın menü öğelerini görüntüleyip görüntülememesini sağlayan yardımcı yöntemleri içerir:


  // parça menüsü
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
...
  // menü yönetimi ------------------------------------------
  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
    // menüdeki tüm öğeleri tarama
    for (int i = 0; i < menu.size(); i++) {
      // i numaralı öğe
      MenuItem menuItem = menu.getItem(i);
      menuOptionsIds.add(menuItem.getItemId());
      // i numaralı öğe bir alt menü ise, işleme yeniden başlanır
      if (menuItem.hasSubMenu()) {
        // rekürsiflik
        getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
      }
    }
  }

  private void getMenuOptionsStates(Menu menu) {
    // sonuç
    if (isDebugEnabled) {
      Log.d(className, "getMenuOptionsStates(Menu)");
    }
    // menü seçeneklerinin kimliklerini alıyoruz
    List<Integer> menuOptionsIds = new ArrayList<>();
    getMenuOptions(menu, menuOptionsIds);
    // menü seçeneklerini bir diziye aktarırız
    menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // seçenek kimliği
      int id = menuOptionsIds.get(i);
      // seçenek durumu
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // sonuç
    if (isDebugEnabled) {
      Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
    }
  }

  // menü seçeneklerinin durumları
  private MenuItemState[] getMenuOptionsStates() {
    MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // durum
      MenuItemState state = this.menuOptionsStates[i];
      // menü kimliği
      int id = state.getMenuItemId();
      // durum başlatma
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // sonuç
    return menuOptionsStates;
  }

  // menü seçeneklerinin görüntülenmesi -----------------------------------
  protected void setAllMenuOptionsStates(boolean isVisible) {
    // tüm menü seçenekleri güncellenir
    for (MenuItemState menuItemState : menuOptionsStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
    }
  }

  protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
    // menüdeki belirli seçenekler güncelleniyor
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}
  • 6-18. satırlar: Bu yöntem, menüdeki tüm seçeneklerin sayısal tanımlayıcılarını almayı sağlar;
  • satır 6: [getMenuOptions] yöntemi iki parametre alır:
    • [Menu menu]: fragmanın menüsü;
    • [List<Integer> menuOptionsIds]: menü seçeneklerinin Android tanımlayıcılarının listesi. Başlangıçta bu liste boştur. Daha sonra menü ağacının özyinelemeli bir taraması (satır 15) ile doldurulur;
  • 20-40. satırlar: menüden yola çıkarak, menü seçeneklerinin durum tablosunu (kimlik, görünürlük) oluşturur. Bu tablo 3. satırda saklanır. [MenuItemState] sınıfı 2.7.1. paragrafta açıklanmıştır;
  • satır 43-55: önceki yöntemin bir varyantıdır. Aynı işlevi yerine getirir, ancak menüdeki tüm seçeneklerin tanımlayıcılarını yeniden hesaplamak yerine (bu işlem daha önce yapılmıştır), 3. satırdaki durum tablosundaki tanımlayıcıları kullanır;
  • satır 58-63: [setAllMenuOptionsStates] yöntemi, parçanın menüsündeki tüm seçenekleri gizlemeye veya göstermeye olanak tanır;
  • satır 65-69: [setMenuOptionsStates] yöntemi, menüdeki belirli seçenekleri seçici bir şekilde görüntülemeyi veya gizlemeyi sağlar;
  • [getMenuOptions, getMenuOptionsStates] yöntemleri, yalnızca [AbstractFragment] içinde kullanıldıkları için özel olarak tanımlanmıştır. [setAllMenuOptionsStates] (satır 58) ve [setMenuOptionsStates] (satır 65) yöntemleri, alt sınıflarda kullanılabilmeleri için korumalı olarak tanımlanmıştır;

2.7.3.4. Asenkron bir görevin tamamlanmasını bekleme yönetimi


   // gözlemlenebilir öğelere abonelikler
  private List<Subscription> abonnements = new ArrayList<>();
// asenkron görevler
  protected int numberOfRunningTasks;
  protected boolean tasksInBackgroundHaveBeenCanceled;
...

  // asenkron bir işlemin tamamlanmasını bekleme yönetimi -------------------------------------
  protected void beginRunningTasks(int numberOfRunningTasks) {
    // çalışacak görevlerin sayısı kaydediliyor
    this.numberOfRunningTasks = numberOfRunningTasks;
    // bekleme görüntüsü eklenir
    mainActivity.beginWaiting();
    // abonelik listesi boşaltılır
    abonnements.clear();
    // henüz iptal yok
    runningTasksHaveBeenCanceled = false;
  }

  protected void cancelWaitingTasks() {
    // bekleme görüntüsü gizlenir
    mainActivity.cancelWaiting();
  }

  • satır 9-18: Bir veya daha fazla asenkron işlemi başlatmak için, alt parça üst parça yöntemi [beginRunningTasks]'i çağıracaktır. Bu yöntemin parametresi, alt parçanın başlatacağı asenkron görevlerin sayısıdır;
  • 11. satır: Yöntemin parametresi saklanır;
  • satır 13: bekleme görüntüsü görünür hale getirilir;
  • satır 15: asenkron işlem abonelik listesi temizlenir. Bu işlemler, alt parça tarafından henüz oluşturulmamıştır;
  • satır 17: alt parça tarafından talep edilen asenkron görevlerin iptal edildiğini belirtmek için bir boole değeri tutulur. Başlangıçta bu boole değerinin değeri false'tir;
  • satır 20-25: alt parça, başlattığı görevleri iptal etmek istediğini belirtmek için [cancelWaitingTasks] adlı üst yöntemini çağırır;
  • 22. satır: bekleme görüntüsü gizlenir;

2.7.3.5. İstisna Yönetimi


  // istisna yönetimi -------------------------------------------------------------------

  // istisna uyarısı görüntüleniyor
  protected void showAlert(Throwable th) {
    // Throwable th istisna yığınındaki mesajlar görüntüleniyor
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
  }

  // mesaj listesini görüntüleme
  protected void showAlert(List<String> messages) {
    // mesaj listesi görüntülenir
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Fermer", null).show();
}
  • satır 4-7: [showAlert(Throwable)] yöntemi, bir alt parçanın, parametre olarak geçirilen Throwable'in istisna yığınındaki mesajları bir pencerede görüntülemesine olanak tanır;
  • satır 10-13: [showAlert(List<String>] yöntemi, bir alt parçanın parametre olarak geçirilen mesaj listesini bir pencerede görüntülemesine olanak tanır;
  • 6. ve 12. satırlarda kullanılan [Utils] sınıfı, 2.7.2 numaralı paragrafta açıklanmıştır;

2.7.3.6. Asenkron işlemlerin yönetimi


...
  // gözlemlenebilir nesnelere abonelikler
  private List<Subscription> abonnements = new ArrayList<>();
  // asenkron görevler
  private boolean runningTasksHaveBeenCanceled;
  protected int numberOfRunningTasks;
...
  // RxAndroid ile asenkron bir görevin yürütülmesi
  protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
    // süreç: yürütülecek / gözlemlenecek gözlemlenebilir
    // consumeResult: Alınan yanıtı işleyen yöntem
    // 
    // Yalnızca iptal olmaması durumunda yeni abonelikler oluşturulur
    if (!runningTasksHaveBeenCanceled) {
      // G/Ç iş parçacığında yürütme ve kullanıcı arayüzü iş parçacığında gözlem
      process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
      // gözlemlenebilir nesne çalıştırılır
      try {
        abonnements.add(process.subscribe(
          // sonucun tüketilmesi
          consumeResult,
          // istisna tüketimi
          new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
              consumeThrowable(th);
            }
          },
          // görev sonu
          new Action0() {

            @Override
            public void call() {
              endOfTask();
            }
          }));
      } catch (Throwable th) {
        consumeThrowable(th);
      }
    }
  }

  private void endOfTask() {
...
  }

  // bir asenkron işlem bir istisna oluşturdu
  // veya bir asenkron işlemin yürütülmesi sırasında bir istisna oluştu
  private void consumeThrowable(Throwable th) {
...
  }

  • 9-41. satırlar: asenkron bir görevi yürütür;
  • 9. satır: [executeInBackground] yöntemi iki parametre bekler:
    • [Observable<T> process]: yürütülecek asenkron işlem;
    • [Action1<T> consumeResult]: Süreç tarafından üretilen öğeleri iletmek üzere çağrılacak alt parçanın yöntemi. Önceki örneklerimizde, süreçler her zaman yalnızca bir öğe üretmiştir. [Action1<T>]'in T türü, gözlemlenen sürecin döndürdüğü sonucun T türüdür;
  • 14. satır: Asenkron görev, kullanıcı veya program tarafından (bir istisna nedeniyle) iptal edilmemişse başlatılır;
  • satır 16: süreç, bir G/Ç iş parçacığı üzerinde çalışacak şekilde yapılandırılmıştır ve UI iş parçacığı üzerinden gözlemlenir;
  • 16. satır: [process.subscribe] komutu, işlemin G/Ç iş parçacığında yürütülmesini başlatır. Bu iş parçacığı içinde işlemler senkron olarak yürütülür, çünkü senkron olan HTTP kütüphanesini kullanıyoruz;
  • 19. satır: [process.subscribe] yöntemi üç parametreye sahiptir:
    • 21. satır: [consumeResult]: İşlem tarafından gönderilen öğeleri alacak olan alt parçanın yöntemi;
    • 22-28. satırlar: Asenkron görevin işlenmesi sırasında bir istisna oluştuğunda yürütülen yöntem. İşlem, 49. satırdaki [consumeThrowable] yöntemine devredilir;
    • satır 29-36: görev, gönderim sonu bildirimini gönderdiğinde yürütülen yöntem. İşlem, satır 43'teki [endOfTask] yöntemine devredilir;
  • 19. satır: Az önce başlatılan asenkron görev, başlatılan tüm asenkron görevleri kaydeden [abonnements] alanına kaydedilir. Bu, gerektiğinde görevlerin iptal edilmesini sağlayacaktır;
  • satır 37-39: Asenkron görevin işlenmesi sırasında bir istisna oluştuğunda yürütülen yöntem. İşlem, 49. satırdaki [consumeThrowable] yöntemine devredilir;

[endOfTask] yöntemi şu şekildedir:


  // asenkron görevler
  protected int numberOfRunningTasks;
...
  private void endOfTask() {
    // bekleyen görev sayısı bir azaldı
    numberOfRunningTasks--;
    // bitti mi?
    if (numberOfRunningTasks == 0) {
      // bekleme sona erdi
      cancelWaitingTasks();
      // alt sınıfa görevlerin bittiği bildirilir
      notifyEndOfTasks(false);
    }
  }
...
  // alt sınıflar -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • 6. satır: Bir asenkron görev az önce tamamlandı. Aktif görev sayacı bir azaltılır;
  • 8. satır: Artık aktif görev kalmadıysa, alt parça tüm yanıtlarını almıştır;
  • 10. satır: bekleme iptal edilir;
  • 12. satır: [notifyEndOfTasks] yöntemini çağırarak, alt parçaya başlattığı tüm görevlerin tamamlandığını bildiririz. Bu yöntemin parametresi, görevlerin nasıl tamamlandığını gösterir: normal şekilde mi, yoksa bir istisna oluştuğu için kullanıcı veya kod tarafından iptal edilerek mi? 12. satırda, normal bir sonlandırma bildirilir. Alt parçanın, hâlâ aktif olan görevlerin sayısını takip etmekle uğraşmasına gerek olmadığına dikkat edilmelidir. Bunu onun yerine üst sınıfı yapar;

[consumeThrowable] yöntemi şu şekildedir:


  // asenkron görevler
  protected int numberOfRunningTasks;
  private boolean runningTasksHaveBeenCanceled;
...
    // bir asenkron işlem bir istisna oluşturdu
  // veya bir asenkron işlemin yürütülmesi sırasında bir istisna meydana geldi
  private void consumeThrowable(Throwable th) {
    // th: işlenecek istisna
    // 
    // günlük
    if (isDebugEnabled) {
      Log.d(className, "Exception reçue");
    }
    // zaten başlatılmış görevler iptal ediliyor
    cancelRunningTasks();
    // hata mesajları görüntüleniyor
    showAlert(th);
  }

  // görevlerin iptal edilmesi
  protected void cancelRunningTasks() {
    // günlük
    if (isDebugEnabled) {
      Log.d(className, "Annulation des tâches lancées");
    }
    // kayıtlı tüm asenkron görevler iptal edilir
    for (Subscription abonnement : abonnements) {
      abonnement.unsubscribe();
    }
    // iptal işlemi kaydedilir
    runningTasksHaveBeenCanceled = true;
    numberOfRunningTasks = 0;
    // bekleme sona erdi
    cancelWaitingTasks();
    // alt parçaya görevlerin iptal edildiği bildirilir
    notifyEndOfTasks(true);
}

...
  // alt sınıflar -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • 3. satır: [consumeThrowable] yöntemi, meydana gelen istisnayı alır;
  • 15. satır: Hala etkin olan tüm görevler iptal edilir;
  • 17. satır: istisna metni görüntülenir;
  • 21-37. satırlar: tüm görevler iptal edilir;
  • satır 27-29: tüm abonelikler iptal edilir;
  • 31. satır: iptal işlemi gerçekleştiği kaydedilir;
  • satır 32: görev sayacı sıfırlanır;
  • 34. satır: bekleme iptal edilir;
  • satır 36: iptal durumunda alt parçaya görevlerin sona erdiği bildirilir;

2.7.3.7. Parçanın yaşam döngüsü yönetimi


  // yaşam döngüsü --------------------------------------------------------
  @Override
  public void onDestroyView() {
    // üst
    super.onDestroyView();
    // günlük
    if (isDebugEnabled) {
      Log.d(className, "onDestroyView");
    }
  }

  @Override
  public void onDestroy() {
    // üst
    super.onDestroy();
    // günlük
    if (isDebugEnabled) {
      Log.d(className, "onDestroy");
    }
  }

  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
...
  }

  private void saveState() {
...
  }

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
...
  }


  @Override
  public void onSaveInstanceState(final Bundle outState) {
...
}
  • 2-20. satırlar: [onDestroyView, onDestroy] yöntemleri yalnızca günlük kaydı amacıyla bulunur. Bu yöntemler, geliştiricinin parçaların yaşam döngüsünü daha iyi kavramasını sağlar;

Cihazın döndürülmesi sırasında parçanın kaydedilmesi, aşağıdaki [setUserVisibleHint, onSaveInstanceState, saveState] yöntemleriyle gerçekleştirilir:


  // parça yaşam döngüsü
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
...

@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // üst öğe
    super.setUserVisibleHint(isVisibleToUser);
    // yedekleme mi?
    if (this.isVisibleToUser && !isVisibleToUser) {
      // parça gizlenecek - yedekleniyor
      if (!saveFragmentDone) {
        saveState();
      }
    }
    // bellek
    this.isVisibleToUser = isVisibleToUser;
  }

  private void saveState() {
...
  }

  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // günlük
    if (isDebugEnabled) {
      Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
    }
    // üst öğe
    super.onSaveInstanceState(outState);
    // parça yalnızca görünürse kaydedilir
    if (isVisibleToUser) {
      // belki de yedekleme zaten yapılmıştır
      if (!saveFragmentDone) {
        saveState();
      }
      // her durumda geri yükleme yapılmalı
      session.setAction(ISession.Action.RESTORE);
    }
}
  • 6-19. satırlar: Parça, görüntüleniyor durumundan gizli duruma geçtiğinde (11. satır) yedeklenir. Bu bilgiyi bize [setUserVisibleHint] yöntemi sağlar;
  • 14. satır: Kaydetme işlemi, 21-23. satırlardaki özel yöntemle gerçekleştirilir;
  • satır 25-41: Cihaz döndürüldüğünde, [onSaveInstanceState] yöntemi çağrılır. Parça, iki koşulda kaydedilir:
    • görünür olması (satır 34);
    • henüz kaydedilmemiş olması (satır 36). Parça görünür durumdayken [setUserVisibleHint, onSaveInstanceState] yöntemlerinin her ikisinin de çalıştırılamaması ve dolayısıyla [saveFragmentDone] boole değerinin yönetilmesinin gereksiz kalması mümkündür. Şüpheye düşmemek için bunu kullanmayı tercih ettim;
  • 40. satır: Kaydetme işleminden sonra geri yükleme işlemi gerçekleşecektir. Parçanın bir dahaki sefere güncellenmesi gerektiğinde, bunun [RESTORE] işlemiyle gerçekleştirilmesi gerektiği unutulmamalıdır;

Parçanın yedeklenmesinin istendiği iki an not edilmelidir:

  1. fragman görünür durumdan gizli duruma geçtiğinde;
  2. cihaz döndürüldüğünde;

[saveState] özel yöntemi şu şekildedir:


...
  private void saveState() {
    // iptal edilecek görevler mi?
    if (numberOfRunningTasks != 0) {
      // görevler iptal ediliyor
      cancelRunningTasks();
    }
    // parçanın durumu yedekleniyor
    CoreState currentState = saveFragment();
    // parça ziyaret edildi
    currentState.setHasBeenVisited(true);
    // menü durumunu kaydediyor
    currentState.setMenuOptionsState(getMenuOptionsStates());
    // oturum açıldı
    session.setCoreState(getNumView(), currentState);
    // kaydetme işlemi tamamlandı
    saveFragmentDone = true;
    // günlük
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }


...
  // alt sınıflar -----------------------------------------------------
public abstract CoreState saveFragment();

protected abstract int getNumView();
  • 4-7. satırlar: Cihazın dönmesi, asenkron işlemler devam ederken gerçekleşebilir. Burada tüm işlemleri iptal etme kararı alınır. Bu, sadece telefonunu veya tabletini hareket ettirmiş ya da bir telefon araması almış olan kullanıcı için iyi bir karar değildir; çünkü kullanıcı potansiyel olarak uzun sürebilecek yeni bir istek yapmak zorunda kalacaktır. Yedekleme/geri yükleme döngüsü boyunca ağ bağlantılarını korumak mümkündür. Ancak çözümler o kadar da açık değildir ve bu başlangıç seviyesindeki derste bu konuyu ele almamaya karar verdim. İzlenecek yol, bu ağ bağlantılarını, görsel arayüzü olmayan ve yedekleme/geri yükleme döngüsü sırasında silinmeyen bir parça aracılığıyla kurmaktır. Bunun için [Fragment.setRetainInstance(true)] komutunu kullanmak yeterlidir;
  • 9. satır: alt parçaya, durumunu [CoreState]'ten türetilmiş bir türde kaydetmesi istenir (31. satır);
  • satır 11: parçanın ziyaret edildiği kaydedilir. Bu bilgi yararlıdır. Bir parça ilk kez ziyaret edildiğinde, oturumda önceki bir durumu olmadığı için güncellemesi sonraki ziyaretlerden farklı olabilir;
  • 13. satır: menünün durumu kaydedilir; bu sayede menüyü otomatik olarak geri yükleyebileceğiz;
  • satır 15: Bu geçerli durum oturumda kaydedilir. Oturumda durumlar görünüm/parça bazında gruplandırılır ve her birinin bir durumu vardır. Görünüm numarası alt parça tarafından sağlanır (satır 33);
  • satır 17: parçanın kaydedildiğini not ediyoruz. Bunun nedeni, iki yöntemin [saveState] yöntemini çağırma olasılığı olması ve iki kez kaydetmenin gereksiz olmasıdır;

Parçayla ilişkili görünümün yeniden oluşturulması aşağıdaki yöntemle sağlanır:


  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // üst sınıf
    super.onActivityCreated(savedInstanceState);
    // günlük
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // görünüm geri yüklenmelidir
    viewHasToBeInitialized = true;
}

Yaşam döngüsünde, [onActivityCreated] yöntemi, [onCreateView] yönteminden hemen sonra yürütülür. Bu son yöntemin çağrılması, parçaya ilişkin görünümün yeniden oluşturulması gerektiğini gösterir. Bunu 10. satırda not etmekle yetiniyoruz.

2.7.3.8. Parçanın güncellenmesi

Parçanın güncellenmesi, parçanın görünür hale gelip kullanıcı eylemlerini beklemeye geçmeden önce üzerinde yapılan son işlemdir. Bu işlem aşağıdaki kodla gerçekleştirilir:


  // parça menüsü
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // parça yaşam döngüsü
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // parça durumları
  private CoreState previousState;
  // jSON eşleştiricisi
  private ObjectMapper jsonMapper = new ObjectMapper();
  // parça yaşam döngüsü
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...

  // Fragman güncellemesi ----------------------------------------------------------------------------------
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // günlük
    if (isDebugEnabled) {
      Log.d(className, "onCreateOptionsMenu");
    }
    // bellek
    this.menu = menu;
    // henüz yapılmadıysa menüdeki # seçenekleri alınır
    if (fragmentHasToBeInitialized) {
      // menüdeki # seçenekleri alınır
      getMenuOptionsStates(menu);
      // etkinlik
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = (Session) this.mainActivity.getSession();
    }
    // parçanın önceki durumunu alır (ilk seferde, yalnızca hasBeenVisited boole değeri bir anlam ifade eder)
    previousState = session.getCoreState(getNumView());
    // alt parçanın birkaç adımda güncellenmesi
    // 1. adım - bu ilk ziyaret mi?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
  ...
    } else {
      // bu ilk ziyaret değil
      // 2. adım: Parça başlatılmalı mı?
      ...
      // 3. adım: Görünüm başlatılmalı mı?
      ...
    }
    // 4. adım: Gönderme, gezinme, geri yükleme mi?
    ...

    // 5. adım: son güncellemeler ----------------------
...
  }
...
  // alt sınıflar -----------------------------------------------------
  protected abstract void initFragment(CoreState previousState);

  protected abstract void initView(CoreState previousState);

  protected abstract void updateOnSubmit(CoreState previousState);

  protected abstract void updateOnRestore(CoreState previousState);

  protected abstract void notifyEndOfUpdates();
  • 19. satır: Parçayı güncellemek için [onCreateOptionsMenu] yöntemi kullanılır. Bu nedenle, parçanın bir menüsü olmalıdır; gerekirse bu menü boş olabilir. Bu yöntem çalıştırıldığında, parça görünür hale gelmiş ve kendi görünümüyle etkinliğine bağlanmıştır;
  • 25. satır: Yönteme parametre olarak aktarılan (22. satır) menü kaydedilir;
  • satır 27-34: Parça başlatılmalıysa:
    • 29. satır: Menü seçeneklerinin durumları, 3. satırdaki [menuOptionsStates] dizisine eklenir;
    • satır 31: aktivite, Android [Activity] türünün bir örneği olarak kaydedilir;
    • satır 32: aktivite, [IMainActivity] arayüzünün bir örneği olarak kaydedilir;
    • satır 33: oturum kaydedilir. [mainActivity.getSession()] yöntemi bir [ISession] türü döndürdüğü için tür değişikliği gereklidir;
  • satır 36: oturumdan parçanın önceki durumu alınır. Parçaya ilk kez ziyaret ediliyorsa, yalnızca [previousState.hasBeenVisited] boole değeri anlamlıdır;
  • satır 39-44: Parçaya ilk kez ziyaret edildiğinde yürütülen kod. Bu durumda, önceki durumu anlamlı değildir;
  • satır 44-50: parçaya ilk ziyaret değilse yürütülen kod;
  • satır 46-47: parça oluşturucusu çağrılmışsa (fragmentHasToBeInitialized==true) yürütülen kod;
  • satır 48-49: parçaya bağlı görünüm yeniden oluşturulmuşsa (viewHasToBeInitialized==true) bu kod çalıştırılır;
  • satır 51-52: o anda devam eden eyleme (SUBMIT, NAVIGATION, RESTORE) göre kod yürütülür;
  • satır 54-55: kodun yürütülmeye devam etmesi;

Güncellemenin beş adımı şunlardır:

1. adım


  // parça menüsü
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // parça yaşam döngüsü
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // parça durumları
  private CoreState previousState;
  // jSON eşleştiricisi
  private ObjectMapper jsonMapper = new ObjectMapper();
  // parça yaşam döngüsü
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // parçanın önceki durumu alınır (ilk seferde, yalnızca hasBeenVisited boole değeri bir anlam ifade eder)
    previousState = session.getCoreState(getNumView());
    // alt parçanın birkaç aşamada güncellenmesi
    // 1. adım - Bu ilk ziyaret mi?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
      // parça ve görünümün başlatılması
      initFragment(null);
      initView(null);
      // devam için previousState sıfırlama
      previousState = null;
    } else {
      // Bu ilk ziyaret değil
...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • satır 19: parçanın önceki durumu oturumdan alınır;
  • 22-31. satırlar: parça daha önce hiç ziyaret edilmediyse kod yürütülür;
  • 27. satır: alt sınıftan parçayı başlatması istenir. 35. satırdaki [initFragment] yönteminin parametresi, parçanın önceki durumudur. Burada, alt parçaya bunun ilk ziyaret olduğunu belirtmek için null değeri aktarılır;
  • 28. satır: alt sınıftan, parçaya bağlı görünümü başlatması istenir. 37. satırdaki [initView] yönteminin parametresi, parçanın önceki durumudur. Burada, alt parçaya bunun ilk ziyaret olduğunu belirtmek için null değeri aktarılır;
  • 30. satır: Takip edecek adımlar için önceki durumu null olarak ayarlıyoruz;

2. ve 3. adımlar


// parça menüsü
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // parçanın yaşam döngüsü
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // parça durumları
  private CoreState previousState;
  // jSON eşleştiricisi
  private ObjectMapper jsonMapper = new ObjectMapper();
  // parça yaşam döngüsü
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // parçanın önceki durumu alınır (ilk seferde, yalnızca hasBeenVisited boole değeri bir anlam ifade eder)
    previousState = session.getCoreState(getNumView());
    // alt parçanın birkaç aşamada güncellenmesi
    // 1. adım - bu ilk ziyaret mi?
    if (!previousState.getHasBeenVisited()) {
...
    } else {
      // bu ilk ziyaret değil
      // 2. adım: Parça başlatılmalı mı?
      if (fragmentHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation fragment");
        }
        // alt fragman
        initFragment(previousState);
      }
      // 3. adım: Görünüm başlatılmalı mı?
      if (viewHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation vue");
        }
        // alt parça
        initView(previousState);
      }
    }

...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • satır 24-42: parçanın ilk ziyareti olmadığı durumlarda yürütülür;
  • satır 27-33: parça yeniden oluşturulmuşsa, alt sınıfın [initFragment] yöntemini çağırarak sıfırlanır (satır 32, 46). Parçanın önceki durumu bu yönteme aktarılır;
  • satır 35-51: parçaya bağlı görünümün başlatılması veya sıfırlanması gerekiyorsa, bunu yapması için alt parçaya talimat verilir (satır 40, 48). Burada da parçanın bilinen son durumu ona aktarılır;

adım 4


// parça menüsü
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // parçanın yaşam döngüsü
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // parçanın durumları
  private CoreState previousState;
  // jSON eşleştiricisi
  private ObjectMapper jsonMapper = new ObjectMapper();
  // parça yaşam döngüsü
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // parçanın önceki durumu alınır (ilk seferde, yalnızca hasBeenVisited boole değeri bir anlam ifade eder)
    previousState = session.getCoreState(getNumView());
    // alt parçanın birkaç aşamada güncellenmesi
 ...

    // 4. adım: bir gönderme, bir gezinme, bir geri yükleme mi?
    // günlük
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("session=%s", jsonMapper.writeValueAsString(session)));
        Log.d(className, String.format("état précédent=%s", jsonMapper.writeValueAsString(previousState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // işlem devam ediyor
    ISession.Action action = session.getAction();
    switch (action) {
      case SUBMIT:
        if (isDebugEnabled) {
          Log.d(className, "updateOnSubmit");
        }
        // alt parça
        updateOnSubmit(previousState);
        break;
      case NAVIGATION:
        if (isDebugEnabled) {
          Log.d(className, "updateForNavigation");
        }
        if (previousState != null) {
          // menü geri yükleme
          setMenuOptionsStates(previousState.getMenuOptionsState());
          // alt parça
          updateOnRestore(previousState);
        } else {
          // bu ilk ziyaret - yapılacak bir şey yok
        }
        break;
      case RESTORE:
        // restorasyon
        if (isDebugEnabled) {
          Log.d(className, "updateOnRestore");
        }
        // menü geri yükleme (previousState null olamaz)
        setMenuOptionsStates(previousState.getMenuOptionsState());
        // alt parça
        updateOnRestore(previousState);
        break;
    }
....
  protected abstract void updateOnSubmit(CoreState previousState);

protected abstract void updateOnRestore(CoreState previousState);
  • satır 34-66: şu anda devam eden eylem işlenir; bu eylem aşağıdakilerden biri olabilir:
    • RESTORE: Cihazın döndürülmesinden sonra parçanın geri yüklenmesi gerçekleştiriliyor;
    • NAVIGATION: parçaya geri dönülürken, onu en son kullanıldığında bırakıldığı durumda bulmaya çalışılır;
    • SUBMIT: diğer tüm durumlar;
  • satır 34: devam eden eylemi geri alıyoruz;
  • satır 36-42: SUBMIT türündeki bir eylem için, alt parçanın [updateOnSubmit] yöntemini (satır 41, 68) çağırır ve parçanın bilinen son durumunu bu yönteme aktarır;
  • satır 43-55: NAVIGATION türündeki bir eylem için;
  • 47-54. satırlar: Parçayı bilinen son durumuna geri getirmek istiyoruz. NAVIGATION işlemi, ilk ziyaretle birleştirilebilir. Bu durum, örneğin sekmeli bir uygulamada geçerli olabilir: 1. sekmeden 4. sekmeye geçersem:
    • ilk ziyaretse, sekme 4'ün parçasını başlatmam gerekir;
    • ilk ziyaret değilse, sekme 4'ün fragmanını önceki durumuna geri getirmeliyim;
  • 52-54. satırlar: İlk ziyaretse hiçbir işlem yapılmaz. Bu başlatma işlemini [initView(CoreState previousState)] alt yöntemi gerçekleştirecektir. İlk ziyaret, [previousState==null] koşulu ile belirlenir;
  • satır 49: Eğer bu, parçanın ilk ziyareti değilse, parçaya menüsü geri verilir;
  • satır 51: satır 70'deki yöntemi çağırarak alt sınıftan kendini güncellemesini istenir. İşini yapabilmesi için fragmanın önceki durumu ona aktarılır;
  • satır 56-66: Parçanın geri yüklenmesi durumunda, ilk ziyaret dışındaki gezinme durumunda yapılan işlemlerin aynısı yapılır;

adım 5


// parça menüsü
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // parça yaşam döngüsü
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // parça durumları
  private CoreState previousState;
  // jSON eşleştiricisi
  private ObjectMapper jsonMapper = new ObjectMapper();
  // parça yaşam döngüsü
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // 5. aşama: son güncellemeler ----------------------
    // görünüm değiştirildi
    session.setPreviousView(getNumView());
    // devam eden işlem yok
    session.setAction(ISession.Action.NONE);
    // bu parçadan çıkıldığında, parçanın kaydedilmesi gerekecek
    saveFragmentDone = false;
    // parça yeniden oluşturulana kadar başlatılmasına gerek yoktur
    fragmentHasToBeInitialized = false;
    // Görünüm yeniden oluşturulana kadar başlatılmasına gerek yoktur
    viewHasToBeInitialized = false;
    // sekme seçiminin normal işleyişine geri dönülür
    session.setNavigationOnTabSelectionNeeded(true);

    // parçaya, görünümün hazır olduğu bildirilir
    if (isDebugEnabled) {
      Log.d(className, "notifyEndOfUpdates");
    }
    notifyEndOfUpdates();
...
  protected abstract void notifyEndOfUpdates();
  • satır 18-30: buraya geldiğimizde, fragman başlatılmış ve görüntülenmeye hazır hale gelmiştir. Ardından, fragmanın yaşam döngüsü yönetiminde kullanılan tüm göstergeleri başlangıç durumuna geri getiririz;
  • 20. satır: Görünüm değiştirildi: bunu oturumda kaydediyoruz;
  • satır 22: Artık devam eden bir eylem yoktur;
  • satır 24: şu anda görüntülenen parçadan çıkılacaksa, çıkarken parçanın kaydedilmesi gerekecektir;
  • satır 26: parçanın yeniden oluşturulmasına artık gerek yoktur. Bu gösterge, parça oluşturucu yeniden çalıştırıldığında vrai değerine sıfırlanacaktır;
  • satır 28: parçaya bağlı görünümün artık başlatılmasına gerek yoktur. Bu gösterge, [onActivityCreated] yöntemi yeniden çalıştırıldığında vrai olarak sıfırlanacaktır;
  • satır 30: Parça, sekmeli bir uygulamada görüntülenebilir. Bu durumda, kullanıcı sekmelerden birine tıkladığında parça değişimi gerçekleşmelidir;
  • satır 36: alt sınıfa parçanın hazır olduğu bildirilir. Alt sınıf, [notifyEndOfUpdates] yöntemine her durumda yapılması gereken güncellemeleri ekleyebilir, yeni verileri almak için asenkron bir işlem başlatabilir, ...

2.7.4. Bir parça örneği

  

[client-android-skel] projesine, okuyucuya bu projeye dayalı bir uygulamanın tipik parça yapısını göstermek amacıyla bir parça örneği eklenmiştir.

[DummyFragment] sınıfı şu şekildedir:


package client.android.fragments.behavior;

import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;

public class DummyFragment extends AbstractFragment {

  // üst sınıftan miras alınan alanlar -------------------------------------------------------

  // hata ayıklama modu
  //-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // sınıf adı
  //-- korumalı String className;
  // asenkron görevler
  //-- korumalı int numberOfRunningTasks;
  // etkinlik
  //-- protected IMainActivity mainActivity;
  //-- korumalı Etkinlik;
  // oturum
  //-- protected Oturum oturum;

  // üst sınıftan miras alınan yöntemler -------------------------------------------------------

  // menü seçeneklerini görüntüleme
  //-- protected void setAllMenuOptionsStates(boolean isVisible) {
  //-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
  // bir dizi asenkron görevin tamamlanmasını bekleme yönetimi
  //-- protected void beginRunningTasks(int numberOfRunningTasks) {
  //-- protected void cancelWaitingTasks() {
  // RxAndroid ile asenkron bir görevin yürütülmesi
  //-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
  // görevlerin iptali
  //-- protected void cancelRunningTasks() {
  // istisna uyarısının görüntülenmesi
  //-- protected void showAlert(Throwable th) {
  // mesaj listesini görüntüleme
  //-- protected void showAlert(List<String> messages) {

  // üst sınıf tarafından zorunlu kılınan yöntemler -------------------------------------------------------

  @Override
  public CoreState saveFragment() {
    // parçayı kaydetmek gerekir
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // eğerkaydedilecek bir şey yoksa [return new CoreState();] komutunu çalıştırın ve [DummyFragmentState] sınıfını silin
  }

  @Override
  protected int getNumView() {
    // Etkinlik tarafından yönetilen parçalar tablosunda parça numarasını geri döndürmek gerekir (bkz. MainActivity)
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // parça görünür hale gelir ve bu adımda veya önceki bir adımda oluşturulmuştur
    // Bu durum, uygulamanın başlatılması sırasında ve Android cihazın her döndürülmesinde gerçekleşir
    // ardından mutlaka [initView]'in yürütülmesi gelir
    // yeniden oluşturulan parçanın alanları başlatılmalıdır
    // previousState, parçanın son yedeklemesidir – parçaya ilk kez ziyaret ediliyorsa null değerindedir
  }

  @Override
  protected void initView(CoreState previousState) {
    // parça görünür hale gelir ve ilgili görünüm bu adımda veya önceki bir adımda yeniden oluşturulmuştur
    // Bu, [initFragment] her çalıştırıldığında ve parça görüntülenen parçanın komşuluğundan her çıktığında gerçekleşir
    // yeniden oluşturulan görünümün bileşenleri başlatılmalıdır
    // previousState, parçanın en son yedeklemesidir – parçaya ilk kez ziyaret ediliyorsa null değerindedir

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    //, bu yöntemler çalıştırılırsa [initFragment, initView]'ten sonra yürütülür
    // SUBMIT türünde bir işlemden sonra görünüm görüntülenecektir
    // genellikle oturumdan parçayı ve ilişkili görünümü başlatmak gerekir
    // previousState, parçanın en son kaydedilmiş halidir – parçaya ilk ziyaret ise null değerindedir
    // SUBMIT işlemiyle parçaya ulaşılamıyorsa yapılacak bir şey yoktur
    // farklı parçalardan SUBMIT işlemleriyle parçaya ulaşılabiliyorsa, önceki görünüm [session.getPreviousView] ile öğrenilebilir
    // Aynı parçadan SUBMIT işlemleriyle bu parçaya ulaşılabiliyorsa, bu parçadan kaynaklanan farklı SUBMIT türlerini ayırt etmek için oturumda bir gösterge tanımlanmalıdır
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    //, bu yöntemler çalıştırıldığında [initFragment, initView]'ten sonra yürütülür
    // görünümü, RESTORE veya NAVIGATION türünde bir işlemden sonra görüntülenecektir
    // previousState, parçanın son yedeklemesidir - asla null değildir
    // görünümü önceki durumuna geri döndürmek gerekir

  }

  @Override
  protected void notifyEndOfUpdates() {
    // [updateOnSubmit, updateOnRestore] yöntemlerinden sonra çalışır
    // buraya geldiğimizde, görünüm oluşturulmuş ve başlatılmış olur
    // burada genellikle yapılacak bir şey yoktur, ancak bu görünüme nasıl ulaşılırsa ulaşılsın yapılması gereken eylemleri burada da faktörlere ayırabiliriz
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // fragman tarafından başlatılan asenkron görevler tamamlandığında veya iptal edildiğinde çağrılır
    // Bu iki durum, runningTasksHaveBeenCanceled parametresi sayesinde ayırt edilebilir
    // genellikle görünüm, asenkron görevlerin yanıtlarını beklerkenki durumundan farklı bir duruma getirilmelidir

  }
}

[DummyFragment] sınıfında durum bulunmayabilir. Burada, içinde ne beklendiğini hatırlatmak amacıyla bir durum eklenmiştir:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class DummyFragmentState extends CoreState {
  // parçanın durumu [DummyFragment]
  // jSON'e yalnızca serileştirilebilir alanlar eklenmelidir
  // diğerlerine @JsonIgnore anotasyonunu ekleyin, ancak bunların ne işe yarayacağı pek belli değil
  // getter ve setter'ları unutmayın – bunlar serileştirme ve ters serileştirme için kullanılır
}

[client-android-skel] projesinin kullanımını açıklamak için, daha kapsamlı bir vaka çalışmasına geçmeden önce basit örnekler kullanacağız.

2.8. Örnek Alıştırmalar

Öncelikle, daha önce yazılmış örnekleri yeniden düzenleyeceğiz.

2.8.1. Örnek-17B

1.18. paragrafta incelenen Örnek 17’yi ele alıyoruz. Bu, asenkron görevler ve sekmeler içermeyen tek bir parçaya sahip bir uygulamadır. Cihaz döndürüldüğünde nasıl davrandığını görmek için bunu inceleyeceğiz. Aşağıdaki girişleri yapıyoruz:

Image

Ardından [1] adresinde cihazı iki kez döndürüyoruz. Yeni görünüm şu şekilde oluyor:

Image

Görünümleri karşılaştırırsak, artık boş olan [2] listesi dışında her şey korunmuştur.

Ayrıca, [Valider] düğmesine tıklandığında, formda yapılan girişleri gösteren bir iletişim kutusu açılır. Bu sırada cihazı döndürürsek, iletişim kutusu kaybolur.

Bu nedenle, döndürme işlemi sırasında şunları yeniden oluşturmamız gerekecektir:

  • açılır listeyi ve seçili öğesini;
  • dönüş sırasında görüntüleniyorsa iletişim kutusunu;

2.8.1.1. [Exemple-17B] projesi

[client-android-skel] projesini exemples/Exemple-17B klasörüne kopyalıyoruz. Ardından yeni [1] projesini yüklüyoruz:

  • [2-3] dosyasına, [behavior] klasörüne, [Exemple-17] projesinden [Vue1Fragment] parçasını yapıştırıyoruz;
  • [4-5]'e, [Exemple-17B]'in [layout] klasörüne, [Exemple-17]'in [vue1.xml] görünümünü yapıştırıyoruz. Bu, parçaya ait görünümdür;
  • [6]'te, [Exemple-17B]'in [values] klasörü, [Exemple-17]'in [values] klasörü ile değiştirilir;

[vue1.xml] görünümünün üst kenar boşluğu 80 dp olarak değiştirilecektir:


    <TextView
      android:id="@+id/textViewFormulaireTitre"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_alignParentTop="true"
      android:layout_marginLeft="10dp"
      android:layout_marginTop="80dp"
      android:text="@string/titre_vue1"
android:textSize="30sp"/>

Bu aşamada, hataları görmek için ilk derlemeyi deneyebiliriz. Bildirilen ilk hatalar, yerleri değişen paketlere ait imports dosyasından kaynaklanmaktadır. Bunları düzeltiriz (Ctrl-Shift-O). Diğer hatalar ise [Vue1Fragment] görünümünün, üst sınıfı olan [AbstractParent] tarafından zorunlu kılınan tüm yöntemleri uygulamamasından kaynaklanmaktadır:

Image

Eksik yöntemleri oluşturuyoruz (Alt-Enter).

Bildirilen bir başka derleme hatası şudur:

Image

Bu durum, modülün [build.gradle] dosyasında (aşağıdaki 20. satır) düzeltilir:

 

Bu aşamada, kalan hataları görmek için yeniden derleme yapabiliriz. Bildirilen tek hata, [Vue1Fragment.updateFragment] yöntemindedir:

 

135. satırdaki [@Override] notunu silmek gerekir. Artık hata kalmamıştır. Projeyi değiştirmek için buradan başlayacağız.

2.8.1.2. [Vue1Fragment] parçasının durumu

[Vue1Fragment] parçası, cihazın döndürülmesi sırasında bilgilerin kaydedilmesini gerektirir; böylece bu bilgiler tamamen geri yüklenebilir. Bunun için [Vue1FragmentState] sınıfını oluşturuyoruz:

  

Şu anda bu sınıf boş:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {
  
}

2.8.1.3. Projeyi özelleştirme

  

[custom] klasöründe, geliştirici tarafından özelleştirilebilen mimari öğeler bulunur.

[IMainActivity] arayüzündeki sabitler şunlar olacaktır:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // oturuma erişim
  ISession getSession();

  // görünüm değişikliği
  void navigateToView(int position, ISession.Action action);

  // bekleme yönetimi
  void beginWaiting();

  void cancelWaiting();

  // uygulama sabitleri -------------------------------------

  // hata ayıklama modu
  boolean IS_DEBUG_ENABLED = true;

  // sunucu yanıtı için maksimum bekleme süresi
  int TIMEOUT = 1000;

  // istemci isteğinin yürütülmesinden önceki bekleme süresi
  int DELAY = 0;

  // temel kimlik doğrulama
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // parçaların bitişikliği
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // sekme çubuğu
  boolean ARE_TABS_NEEDED = false;

  // bekleme görüntüsü
  boolean IS_WAITING_ICON_NEEDED = false;

  // uygulamanın parça sayısı
  int FRAGMENTS_COUNT = 1;

}
  • 24-31. satırlar: Uygulama burada [DAO] katmanını kullanmaz. Bu sabitler kullanılmayacaktır;
  • satır 34: varsayılan değer olan 1'lik parça komşuluğu. Uygulamada yalnızca bir parça (satır 43) olduğundan, bu değerin bir önemi yoktur;
  • satır 39-40: [DAO] katmanıyla ilgili herhangi bir işlem olmadığı için, bekleme görüntüsüne gerek yoktur;
  • satır 37: bu, sekmeli bir uygulama değildir;
  • 43. satır: Yalnızca bir fragman vardır;

[Session] sınıfı şu şekildedir:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // jSON formatında serileştirilemeyen öğeler @JsonIgnore etiketine sahip olmalıdır

}

Bu sınıf boştur. Nitekim, sadece bir parça olduğu için, bir oturumla parçalar arası iletişim sağlanmasına gerek yoktur.

Son olarak, [CoreState] sınıfı şöyledir:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
  // fragman ziyaret edildi mi, edilmedi mi
  protected boolean hasBeenVisited = false;
  // parçanın menüsünün durumu (varsa)
  protected MenuItemState[] menuOptionsState;

  // getter ve setter'lar
...
}
  • 11-13. satırlar: Farklı parçaların durumunu kaydeden, [CoreState] sınıfından türetilen tüm sınıfları buraya eklememiz gerekir. Burada sadece bir tane vardır (12. satır);

2.8.1.4. [MainActivity] etkinliği

[MainActivity] etkinliği şu anda şu şekildedir:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // [DAO] katmanı
  @Bean(Dao.class)
  protected IDao dao;
  // oturum
  private Session session;

  // üst sınıf yöntemleri -----------------------
  @Override
  protected void onCreateActivity() {
    // günlük
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // oturum
    this.session = (Session) super.session;
    // yapılacaklar: üst sınıf tarafından başlatılan başlatma işlemlerine devam edelim
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // yapılacaklar: parçaları burada tanımlayın
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // yapılacaklar: parçaların başlıklarını burada tanımlayın
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // yapılacaklar: sekmeli gezinme - [position] numaralı sekme seçildiğinde görüntülenecek görünümü tanımlayın
  }

  @Override
  protected int getFirstView() {
    // yapılacaklar: görüntülenecek ilk görünümün (parça) numarasını belirleyin
    return 0;
  }
}

[//todo] yorumları, geliştiricinin ne yapması gerektiğini belirtir. [MainActivity] sınıfı şu şekilde gelişir:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // katman [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // oturum
  private Session session;

  // üst sınıf yöntemleri -----------------------
  @Override
  protected void onCreateActivity() {
    // günlük
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // oturum
    this.session = (Session) super.session;
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new Vue1Fragment_()};
  }

  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {

  }

  @Override
  protected int getFirstView() {
    return 0;
  }
}

Yalnızca 41-44. satırlardaki yöntem değiştirilmelidir. Bu yöntem, uygulamanın parça dizisini döndürmelidir. 43. satırda, parça adının arkasına alt çizgi koymayı unutmamak gerekir.

2.8.1.5. [FragmentState] fragmanının durumu

[Exemple-17] projesinde yapılan rotasyon testlerinin ardından, parçanın aşağıdaki öğelerinin kaydedilmesine karar verilir:

  • açılır menüdeki değerlerin listesi;
  • bu listede seçili öğenin konumu;
  • dönüş sırasında diyalog kutusu mevcutsa, bu kutuda görüntülenen mesaj;

[Vue1FragmentState] sınıfı aşağıdaki gibi olacaktır:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

import java.util.List;

public class Vue1FragmentState extends CoreState {

  // açılır listedeki değerler
  private List<String> list;
  // açılır listeden seçilen öğe
  private int listSelectedPosition;
  // iletişim kutusunda görüntülenen mesaj
  private String message;

  // alıcı ve ayarlayıcılar
...
}

2.8.1.6. [AbstractFragment] parçası

Şu anda parçanın yaşam döngüsü iki yöntemle yönetilmektedir (6. ve 32. satırlar):


// açılır liste
  private List<String> list;
  private ArrayAdapter<String> dataAdapter;

  @AfterViews
  void afterViews() {
    // ilk düğme işaretlenir
    radioButton1.setChecked(true);
    // takvim
    datePicker1.setCalendarViewShown(false);
    // seekBar
    seekBar.setMax(100);
    seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {

      public void onStopTrackingTouch(SeekBar seekBar) {
      }

      public void onStartTrackingTouch(SeekBar seekBar) {
      }

      public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        seekBarValue.setText(String.valueOf(progress));
      }
    });
    // açılır liste
    list = new ArrayList<>();
    list.add("list 1");
    list.add("list 2");
    list.add("list 3");
  }
...
  protected void updateFragment() {
    // açılır menü adaptatörünün başlatılması
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    dropDownList.setAdapter(dataAdapter);
  }

Bu iki yöntemin kodu, [AbstractFragment] sınıfı tarafından belirlenen yöntemlere şu şekilde taşınacaktır:


// parça yaşam döngüsü yönetimi ---------------------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    Vue1FragmentState state = new Vue1FragmentState();
    state.setList(list);
    state.setListSelectedPosition(dropDownList.getSelectedItemPosition());
    state.setMessage(message);
    return state;
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // İlk ziyaret mi?
    if (previousState == null) {
      // açılır listenin değerleri oluşturulur
      list = new ArrayList<>();
      list.add("list 1");
      list.add("list 2");
      list.add("list 3");
    } else {
      // açılır listenin değerleri geri yükleniyor
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      list = state.getList();
      // ve iletişim kutusundaki mesaj
      message = state.getMessage();
    }
    // açılır listenin adaptörünü başlatma
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
  }

  @Override
  protected void initView(CoreState previousState) {
    // takvim
    datePicker1.setCalendarViewShown(false);
    // seekBar
    seekBar.setMax(100);
    seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {

      public void onStopTrackingTouch(SeekBar seekBar) {
      }

      public void onStartTrackingTouch(SeekBar seekBar) {
      }

      public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        seekBarValue.setText(String.valueOf(progress));
      }
    });
    // açılır liste adaptörünün başlatılması
    dropDownList.setAdapter(dataAdapter);
    // İlk ziyaret mi?
    if (previousState == null) {
      // ilk düğmeyi işaretliyoruz
      radioButton1.setChecked(true);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // kaydırma çubuğunun değeri
    seekBarValue.setText(String.valueOf(seekBar.getProgress()));
    // açılır listeden seçilen öğe
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    dropDownList.setSelection(state.getListSelectedPosition());
    // diyalog penceresi görünür mü?
    if (message != null) {
      // görüntüleniyor
      showMessage();
    }
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

}
  • 2-9. satırlar: [saveFragment] yöntemi, saklanacak parça öğelerini [CoreState] sınıfından türetilmiş bir sınıfa yerleştirmeli ve bu sınıfın örneğini döndürmelidir;
  • 11-14. satırlar: [getNumView] yöntemi, parçanın numarasını döndürmelidir. Burada, numarası 0 olan tek bir parça vardır;
  • satır 16-34: [initFragment] yöntemi, parçanın alanlarını başlatmalıdır. Yöntem, parçanın önceki durumunu alır. Eğer [previousState] değeri null ise, bu durumda ilk ziyaret söz konusudur;
  • satır 19-25: ilk ziyaret sırasında açılır menünün değerleri oluşturulur;
  • satır 26-30: ilk ziyaret değilse, parçadaki [list, message] alanları önceki durumdan geri yüklenir;
  • satır 33-34: Parçanın [dataAdapter] alanı başlatılır. Bu, açılır listenin veri kaynağıdır;
  • satır 37-62: [initView] yöntemi, görsel arayüz bileşenlerini başlatmak için kullanılır. Bu yöntem, parametre olarak önceki durumu ([previousState]) alır. Eğer [previousState==null] ise, bu ilk ziyaret demektir;
  • burada, daha önce [@AfterViews] yönteminde bulunanlar yer almaktadır;
  • 57-61. satırlar: İlk ziyaret sırasında, ilk radyo düğmesinin işaretli olduğundan emin olunur;
  • 64-67. satırlar: [updateOnSubmit] yöntemi, mevcut eylem [SUBMIT] olduğunda yürütülür. Burada parçalar arası gezinme olmadığı için mevcut bir eylem de yoktur;
  • satır 69-81: [updateOnRestore] yöntemi, mevcut eylem [NAVIGATION] veya [RESTORE] olduğunda yürütülür. Burada parçalar arası gezinme yoktur ve dolayısıyla [NAVIGATION] eylemi mümkün değildir;
  • 72. satır: TextView seekBarValue değerini yeniden hesaplıyoruz (geri yüklemiyoruz). Zira, döndürme işlemleri sırasında bu değer bazen kayboluyordu;
  • 74-75. satırlar: Liste, döndürme işleminden önce seçili olan öğenin üzerine konumlandırılır. Aksi takdirde, liste ilk öğesinin üzerine konumlanıyordu;
  • satır 76-80: önceki durum mesajı null değilse, iletişim kutusu yeniden görüntülenir. [showMessage] yöntemine (satır 79) daha sonra tekrar değineceğiz;
  • satır 83-86: [notifyEndOfUpdates] yöntemi, alt parçayı kendi haline bırakmadan önce üst sınıf tarafından çağrılan son yöntemdir. Burada yapılacak bir şey yoktur;
  • satır 88-91: [notifyEndOfTasks] yöntemi, parça tarafından başlatılan asenkron görevlerin sonunu bildirir. Burada böyle bir görev yoktur;

Diyalog kutusunun geri yüklenmesi şu şekilde gerçekleştirilir:


  // diyalog kutusundaki mesaj
  private String message;
...
  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // görüntülenecek mesaj listesi
    List<String> messages = new ArrayList<>();
    ...
    // görüntüleme
    doAfficher(messages);
  }

  private void doAfficher(final List<String> messages) {
    // görüntülenecek metin oluşturuluyor
    StringBuilder texte = new StringBuilder();
    for (String message : messages) {
      texte.append(String.format("%s\n", message));
    }
    // mesaj kaydediliyor
    message = texte.toString();
    // mesaj görüntüleniyor
    showMessage();
  }

  private void showMessage() {
    // mesaj görüntüleniyor
    new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        // mesaj sıfırlanıyor
        message = null;
      }
    }).show();
}

Kullanıcı formu onayladığında, [doValider] yöntemi (satır 5) bir mesaj listesi oluşturur ve bunu daha sonra (satır 10) iletişim kutusunda görüntüler.

  • 14-20. satırlar: Mesaj listesi tek bir mesaj halinde birleştirilir ve 2. satırda saklanır;
  • 25-33. satırlar: İletişim kutusunda görüntülenen mesaj budur ve [updateOnRestore] yöntemi de aynı mesajı görüntületir;
  • 27. satır: [setNeutralButton] yönteminin ikinci parametresi, kullanıcı iletişim kutusundaki [Fermer] düğmesine tıkladığında yürütülen yöntemdir;
  • 31. satır: İletişim kutusu kapatıldığında, iletişim kutusunun artık mevcut olmadığını belirtmek için mesaj null olarak güncellenir;

2.8.1.7. Tests

Okuyucunun bu projeyi test etmesi ve bir veya daha fazla ardışık döndürme işleminden sonra parçanın korunduğunu doğrulaması istenir.

2.8.2. Örnek-23: Hava durumu istemcisi

Bazı siteler, jSON dizileri biçiminde hava durumu bilgileri sunar. İşte bir örnek:

Image

URL şu biçimdedir: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID} burada:

  • city: hava durumunu öğrenmek istediğimiz şehir, burada Angers;
  • country: şehrin bulunduğu ülke, burada Fransa (fr);
  • APPID: [https://home.openweathermap.org/users/sign_up] sitesine kaydolarak elde edilen bir anahtar;

2.8.2.1. Proje

  

Proje, [client-android-skel] projesinden yola çıkılarak oluşturulmuştur. Aşağıdaki özelliklere sahiptir:

  • sadece tek bir parçası vardır ve bu parçanın durumunu korumak gerekmez;
  • asenkron istekler yapar;

2.8.2.2. Projenin özelleştirilmesi

  

[IMainActivity] arayüzü, projenin belirli özelliklerini belirlemeye olanak tanır:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // oturuma erişim
  ISession getSession();

  // görünüm değiştirme
  void navigateToView(int position, ISession.Action action);

  // bekleme yönetimi
  void beginWaiting();

  void cancelWaiting();

  // uygulama sabitleri -------------------------------------

  // hata ayıklama modu
  boolean IS_DEBUG_ENABLED = true;

  // sunucu yanıtı için maksimum bekleme süresi
  int TIMEOUT = 1000;

  // istemci isteğinin yürütülmesinden önceki bekleme süresi
  int DELAY = 5000;

  // temel kimlik doğrulama
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // parçaların bitişikliği
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // sekme çubuğu
  boolean ARE_TABS_NEEDED = false;

  // bekleme görüntüsü
  boolean IS_WAITING_ICON_NEEDED = true;

  // uygulamanın parça sayısı
  int FRAGMENTS_COUNT = 1;

}
  • 25, 28, 31, 40. satırlar: [DAO] katmanının özellikleri. 31. satırda, temel kimlik doğrulamaya gerek yoktur;
  • satır 34: parçaların bitişikliği. Burada tek bir parça olduğu için bu sabitin önemi yoktur;
  • satır 37: bu, sekmeli bir uygulama değildir;
  • satır 43: tek bir fragman vardır;

Parçaların durumunu saklayan [CoreState] sınıfı şu şekilde olacaktır:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// yapılacaklar: buraya [CoreState]'in alt sınıflarını ekleyin
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // ziyaret edilmiş veya edilmemiş parça
  protected boolean hasBeenVisited = false;
  // parçanın olası menüsünün durumu
  protected MenuItemState[] menuOptionsState;

  // getter ve setter'lar
...
}
  • 10-13. satırlar: Bu uygulamada durumu saklanmayan tek bir parça olduğu için bildirilecek bir şey yoktur;

[Session] sınıfı şu şekildedir:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // jSON'te serileştirilemeyen öğeler @JsonIgnore ek etiketine sahip olmalıdır
}

Bu uygulamada parçalar arası iletişim olmadığı için bu sınıf boştur.

2.8.2.3. [DAO] katmanı

  

[DAO] katmanında üç sınıfın özelleştirilmesi gerekir:

  • IDao arayüzü;
  • Dao uygulaması;
  • web sunucusuyla iletişim kuran WebClient arayüzü / jSON;

[WebClient] arayüzü şu şekilde olacaktır:


package client.android.dao.service;

import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // hava durumu hizmeti
  @Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
  String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
  • 18-19. satırlar: hava durumu hizmetine ait URL. Bunun, istemcinin kök URL'iyle (RestClientRootUrl, satır 12) ilişkili olduğu hatırlatılmalıdır. Burada bu kök URL, [http://api.openweathermap.org/] olacaktır;

[IDao] arayüzü şu şekilde olacaktır:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // web hizmetinin URL'si
  void setUrlServiceWebJson(String url);

  // kullanıcı
  void setUser(String user, String mdp);

  // istemci zaman aşımı
  void setTimeout(int timeout);

  // temel kimlik doğrulama
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // hata ayıklama modu
  void setDebugMode(boolean isDebugEnabled);

  // istek öncesi istemci bekleme süresi (milisaniye cinsinden)
  void setDelay(int delay);

  //  hava durumu hizmeti
  Observable<String> getWeatherForecast(String city, String country, String APPID);
}
  • Hatırlatmak gerekirse, 6-22. satırlardaki yöntemler, [client-android-skel] projesinin IDao arayüzünde varsayılan olarak mevcuttur;
  • 25. satır: [getWeatherForecast] yöntemi, [country] ülkesindeki [city] şehrinin hava durumu bilgilerini içeren jSON dizesini elde etmeyi sağlar. 3. parametre, [https://home.openweathermap.org/users/sign_up] sitesinden alınan anahtardır;

[IDao] arayüzü, aşağıdaki [Dao] sınıfı tarafından uygulanır:


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // web hizmeti istemcisi
  @RestService
  protected WebClient webClient;
  // güvenlik
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // RestTemplate
  private RestTemplate restTemplate;
  //  RestTemplate fabrikası
  private SimpleClientHttpRequestFactory factory;
  // zaman aşımı
  private int timeout;

  @AfterInject
  public void afterInject() {
    // günlük
    Log.d(className, "afterInject");
    // restTemplate oluşturuluyor
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // jSON dönüştürücüsü ayarlanıyor
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // web istemcisinin restTemplate'i belirleniyor
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // web hizmetinin URL'i belirlenir
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // kullanıcı, yakalayıcıya kaydedilir
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // bellek
    this.timeout = timeout;
    // fabrika yapılandırması
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // kimlik doğrulama yakalayıcısı mı?
    if (isBasicAuthentificationNeeded) {
      // kimlik doğrulama interceptor'u ekleniyor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }


  // özel yöntemler -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // hava durumu hizmeti ---------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
    // günlük
    if (isDebugEnabled) {
      Log.d(className, String.format("getWeatherForecast city=%s, country=%s, APIID=%s, thread=%s, timeout=%s", city, country, APPID, Thread.currentThread().getName(), timeout));
    }
    // sonuç
    return getResponse(new IRequest<String>() {
      @Override
      public String getResponse() {
        return webClient.getWeatherForecast(city, country, APPID);
      }
    });
  }
}
  • 17-90. satırların, [client-android-skel] projesindeki [Dao] sınıfında varsayılan olarak mevcut olduğu hatırlatılmalıdır. Sadece uygulamaya özgü [IDao] arayüzünün uygulama yöntemlerini (92. satır) eklemek yeterlidir;
  • 93-105. satırlar: [getWeatherForecast] yönteminin uygulaması. Bu çok basittir ve 6 satırda gerçekleştirilir (100-105. satırlar);
  • 100. satır: [getResponse] yöntemi, üst sınıf olan [AbstractDao]'in bir yöntemidir. Bu yöntem, [IRequest<T>] türünde bir parametre bekler; burada T, sunucudan beklenen yanıtın türüdür. Bu örnekte, bir jSON dizesi beklendiğinden, T'nin türü String'tir. [IRequest<T>]'in T türü, [Observable<T> getWeatherForecast] yönteminin T türü olmalıdır;
  • [IRequest<T>] arayüzünün tek bir yöntemi vardır: getResponse. Bu yöntemin görevi, [Observable<T> getWeatherForecast] yönteminin döndürmesi gereken T türündeki yanıtı sağlamaktır;
  • 103. satır: Bu yanıtı sağlayan, [WebClient] arayüzüdür. 94. satırda alınan üç parametre bu arayüze aktarılır. Bu nedenle, bu parametrelerin final özniteliğine sahip olması gerekir;

2.8.2.4. [MainActivity] etkinliği

  

[MainActivity] etkinliği şöyledir:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.MeteoFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // katman [DAO]
  @Bean(Dao.class)
  protected IDao dao;

  // üst sınıf yöntemleri -----------------------
  @Override
  protected void onCreateActivity() {
    // günlük
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new MeteoFragment_()};
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
  }

  @Override
  protected int getFirstView() {
    return 0;
  }

  // arayüz IDao ---------------------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(String city, String country, String APPID) {
    return dao.getWeatherForecast(city, country, APPID);
  }
}
  • [client-android-skel] projesinde 15-55. satırların varsayılan olarak mevcut olduğunu hatırlatmak isteriz. Bunları sadece özelleştirmeniz yeterlidir;
  • 37-40. satırlar: parça tablosu. Burada sadece bir tane var;
  • 43-46. satırlar: parça başlıklarına gerek yok;
  • 48-50. satırlar: burada sekme yok;
  • 52-55. satırlar: görüntülenecek ilk görünüm, [MeteoFragment] numaralı 0. görünümdür;
  • satır 58-61: [IDao] arayüzünün uygulanması. Burada, işi 21. satırdaki [DAO] katmanına devretmekten başka yapılacak bir şey yoktur;

2.8.2.5. [MeteoFragment] parçası

  

[MeteoFragment] parçası, /jSON hava durumu web hizmetine sorgu gönderir. Temel yapısı şöyledir:


package client.android.fragments;

import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.AbstractFragment;
import client.android.architecture.MenuItemState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action0;
import rx.functions.Action1;

@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class FirstFragment extends AbstractFragment {
...
}
  • 14. satır: [res / layout / meteo_fragment.xml] görünümü şu şekildedir:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="Construisez votre interface visuelle"
    android:id="@+id/textView" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true" android:layout_marginLeft="64dp" android:layout_marginStart="64dp"
    android:layout_marginTop="120dp"/>
</RelativeLayout>

Görünümde yalnızca 10. satırdaki metin görüntülenir;

  • 15. satır: [res / menu / menu_meteo.xml] menüsü şu şekildedir:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionMeteo"
        android:title="@string/actionMeteo"/>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
      <item
        android:id="@+id/actionTerminer"
        android:title="@string/actionTerminer"/>
    </menu>
  </item>
</menu>
  • 10-12. satırlar: Bu menü seçeneği, bir şehrin hava durumunu sorgulamak için kullanılır;
  • 14-15. satırlar: Bu menü seçeneği, devam eden bir sorguyu iptal etmek için kullanılır;
  • satır 16-18: Bu menü seçeneği uygulamayı sonlandırır;

Parçanın tam kodu şöyledir:


package client.android.fragments.behavior;

import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action1;

@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class MeteoFragment extends AbstractFragment {

  // yerel veriler
  private int nbReponsesRecues;

  // olay yönetimi ---------------------------------------------------------------------------------------
  // hava durumunu öğrenmek istediğimiz şehirler
  final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

  @OptionsItem(R.id.actionMeteo)
  protected void doMeteo() {
    // ülkesi
    String country = "fr";
    // bir hesap oluşturarak bir kimlik alın API [https://home.openweathermap.org/users/sign_up]
    String APPID = "xyz";
    // web hizmetinden URL / jSON
    mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
    // [paysDeLoire.length] asenkron görevlerinin bekleme süresinin başlangıcı
    beginWaiting(paysDeLoire.length);
    // alınan yanıt sayısı
    nbReponsesRecues = 0;
    // asenkron çağrılar paralel olarak gerçekleştirilir
    for (String city : paysDeLoire) {
      // hava durumu
      executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
        @Override
        public void call(String response) {
          // yanıtın işlenmesi
          consumeResponse(response);
          // bir yanıt +
          nbReponsesRecues++;
        }
      });
    }
  }

  // sunucu yanıtının işlenmesi
  private void consumeResponse(String response) {
    // günlük
    Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
  }

  // bekleme başlangıcı
  protected void beginWaiting(int numberOfRunningTasks) {
    // günlük
    if (isDebugEnabled) {
      Log.d(className, "beginWaiting");
    }
    // üst öğe
    beginRunningTasks(numberOfRunningTasks);
    // [Annuler] seçeneği görüntüleniyor
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{
      new MenuItemState(R.id.menuActions, true),
      new MenuItemState(R.id.actionAnnuler, true)});

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menü
    initMenu();
    // sonuçların görüntülenmesi
    String message;
    switch (nbReponsesRecues) {
      case 0:
        message = "Aucune réponse n'a été reçue";
        break;
      case 1:
        message = "Une réponse a été reçue. Consultez vos logs...";
        break;
      default:
        message = String.format("%s réponses ont été reçues. Consultez vos logs...", nbReponsesRecues);
        break;
    }
    Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
  }

  // özel yöntemler -----------------------------------
  private void initMenu() {
    if (isDebugEnabled) {
      Log.d(className, "initMenu");
    }
    // menü
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }

  // yaşam döngüsü yönetimi ---------------------------------------------------------------------------------------
...
}
  • satır 25-50: [Météo] menü seçeneğine yapılan tıklamanın işlenmesi;
  • satır 32: web hizmeti URL / hava durumu hizmeti jSON'in oluşturulması. Bu daha sonra etkinlik aracılığıyla [DAO] katmanına aktarılır;
  • satır 34: bekleme süreci başlıyor. Başlatılacak görev sayısını aktarıyoruz; bu, üst sınıfın görevlerin tamamlandığını bize bildirebilmesi içindir. Burada beş görev var çünkü 23. satırdaki beş şehrin hava durumunu isteyeceğiz;
  • 16. satır: Ekrana görüntüleyebilmek için alınan yanıtların sayısını sayıyoruz;
  • satır 38-50: hava durumunu öğrenmek istediğimiz şehirler üzerinde döngü yapıyoruz;
  • satır 40: 5 adet HTTP isteğini paralel olarak göndereceğiz;
  • 40. satır: üst sınıf [AbstractParent]'ten / jSON web hizmetini sorgulamasını istiyoruz;
  • satır 40-48: [executeInBackground] yöntemi iki parametre bekler:
    • 40. satır: Gözlemlenecek ve yürütülecek işlem, [mainActivity.getWeatherForecast] yöntemi tarafından sağlanır;
    • satır 40-48: Asenkron hizmetten yanıt alındığında yürütülecek [Action1] örneği. [Action1<T>]'in T türü, [getWeatherForecast] yönteminin sonucunun T türü olmalıdır;
  • satır 44: bir yanıt alındı. Bu yanıt, 53. satırdaki [consumeResponse] yöntemine aktarılır;
  • 46. satır: Alınan yanıt sayacı artırılır;
  • satır 53-56: hava durumu hizmetinden gelen jSON yanıtı işlenir;
  • satır 55: jSON dizesi sadece günlüğe kaydedilir;
  • 59-72. satırlar: asenkron görevler başlatılmadan önce yürütülen kod;
  • 65. satır: Yürütülecek görev sayısını üst sınıf olan [AbstractParent]'e aktarılır. Bu sayede, tüm görevler tamamlandığında bu sınıf bize bildirimde bulunur;
  • satır 67-70: bekleme menüsünün hazırlanması. Yalnızca kullanıcının başlatılan görevleri iptal etmesine olanak tanıyan [Actions/Annuler] seçeneği tutulur;
  • satır 74-92: üst sınıf, başlatılan tüm görevlerin tamamlandığını bildirdiğinde yürütülen kod;
  • 77. satır: menüyü başlangıç durumuna geri getiririz. [initMenu] yöntemi (95-102. satırlar), gizlenen [Actions/Annuler] seçeneği hariç tüm seçenekleriyle menüyü görüntüler;
  • satır 80-91: Alınan yanıtların sayısı görüntülenir;

[Annuler] menü seçeneğine tıklama, aşağıdaki kodla yönetilir:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asenkron görevleri iptal ediyoruz
    cancelRunningTasks();
}
  • 7. satır: üst sınıftan halen etkin olan görevleri iptal etmesi istenir;

[Terminer] menü seçeneğine tıklama, aşağıdaki kodla yönetilir:


  @OptionsItem(R.id.actionTerminer)
  protected void doTerminer() {
    // her şeyi durdur
    System.exit(0);
}

Parçanın yaşam döngüsü yönetimi aşağıdaki yöntemlerle sağlanır:


  // yaşam döngüsü yönetimi ---------------------------------------------------------------------------------------

  @Override
  public CoreState saveFragment() {
    return new CoreState();
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {

  }

  @Override
  protected void initView(CoreState previousState) {
    // İlk ziyaret mi?
    if (previousState == null) {
      initMenu();
    }
  }


  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

}
  • 3-6. satırlar: parçanın durumunu [CoreState] sınıfından türetilen bir sınıfta kaydetmeye yarar. Burada olduğu gibi parçanın kaydedilecek bir durumu yoksa, sadece bir [CoreState] örneği döndürülür. null döndürülmemelidir, çünkü bu daha sonra bir çökmeye yol açar;
  • 8-11. satırlar: görünüm numarasını döndürmelidir. Burada [MeteoFragment] parçacığının numarası 0'dır;
  • satır 13-16: parça oluşturulduktan (previousState==null) veya yeniden oluşturulduktan (previousState!=null) sonra onu başlatmak için kullanılır. Burada yapılacak bir şey yoktur. Başlatılabilecek tek alan şudur:

  // hava durumunu öğrenmek istediğimiz şehirler
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

ancak bu alan kendiliğinden başlatılır;

  • 18-24. satırlar: parça oluşturulduktan (previousState==null) veya yeniden oluşturulduktan (previousState!=null) sonra, parçayla ilişkili görünümü başlatmak için kullanılır;
  • 21-23. satırlar: parçaya ilk kez ziyaret ediliyorsa, [Annuler] seçeneğini gizlemek için menüsü başlatılır;
  • satır 27-30: Parçaya ulaşmak için [SUBMIT] türünde bir eylemle gezinme yapılmışsa bu satırlar çağrılır. Burada, tek bir parça olduğu için parçalar arası gezinme yoktur;
  • satır 32-35: Cihazın döndürülmesi veya başka bir nedenden dolayı bir yedekleme/geri yükleme döngüsü sırasında çağrılır. Burada, durum yedeklenmediğinden yapılacak bir şey yoktur;
  • satır 37-40: önceki tüm güncellemeler tamamlandığında çağrılır. Burada yapılacak bir şey yoktur;

2.8.2.6. Tests

Şimdi örneği çalıştırıyoruz:

Image

Image

Günlük kayıtları şu şekildedir:


07-23 13:24:30.899 2642-2642/client.android D/MainActivity_: constructeur
07-23 13:24:30.945 2642-2642/client.android D/AbstractDao: constructeur, thread=main
07-23 13:24:32.861 2642-2642/client.android D/client.android.dao.service.Dao_: afterInject
07-23 13:24:32.950 2642-2642/client.android D/MainActivity_: onCreate
07-23 13:24:32.951 2642-2642/client.android D/client.android.dao.service.Dao_: setTimeout thread=main, timeout=1000
07-23 13:24:32.952 2642-2642/client.android D/client.android.dao.service.Dao_: setBasicAuthentification thread=main, isBasicAuthentificationNeeded=false
07-23 13:24:33.041 2642-2642/client.android D/MainActivity_: adding loadingPanel
07-23 13:24:33.043 2642-2642/client.android D/MeteoFragment_: constructeur
07-23 13:24:33.044 2642-2642/client.android D/MainActivity_: navigation vers vue 0 sur action NONE
07-23 13:24:33.044 2642-2642/client.android D/MainActivity_: onCreateActivity
07-23 13:24:33.080 2642-2642/client.android D/MainActivity_: onResume
07-23 13:24:33.325 2642-2642/client.android D/MeteoFragment_: onActivityCreated
07-23 13:24:33.518 2642-2642/client.android D/MeteoFragment_: onCreateOptionsMenu
07-23 13:24:33.518 2642-2642/client.android D/MeteoFragment_: getMenuOptionsStates(Menu)
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: Nombre d'options de menu=4
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: initFragment initView updateForFirstVisit
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: initMenu
07-23 13:24:33.557 2642-2642/client.android D/MeteoFragment_: session={"action":"NONE","coreStates":[{"@type":"CoreState","hasBeenVisited":false,"menuOptionsState":null}],"previousTab":0,"previousView":0}
07-23 13:24:33.557 2642-2642/client.android D/MeteoFragment_: état précédent=null
07-23 13:24:33.558 2642-2642/client.android D/MeteoFragment_: notifyEndOfUpdates
07-23 13:24:39.766 2642-2642/client.android D/MeteoFragment_: beginWaiting
07-23 13:24:39.831 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=angers, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.831 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.882 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=le mans, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.882 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.885 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=nantes, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.885 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.886 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=laval, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.886 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.887 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=la roche sur yon, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.887 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:45.035 2642-2961/client.android D/client.android.dao.service.Dao_: response={"coord":{"lon":-1.55,"lat":47.22},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"cmc stations","main":{"temp":298.05,"pressure":1022,"humidity":47,"temp_min":297.15,"temp_max":299.15},"wind":{"speed":2.6,"deg":310},"clouds":{"all":0},"dt":1469277000,"sys":{"type":1,"id":5641,"message":0.0032,"country":"FR","sunrise":1469248505,"sunset":1469303378},"id":2990969,"name":"Nantes","cod":200} sur thread [RxIoScheduler-4]
07-23 13:24:45.035 2642-2963/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-6]
07-23 13:24:45.035 2642-2959/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-2]
07-23 13:24:45.035 2642-2962/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-5]
07-23 13:24:45.036 2642-2960/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-3]
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={"coord":{"lon":-1.55,"lat":47.22},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"cmc stations","main":{"temp":298.05,"pressure":1022,"humidity":47,"temp_min":297.15,"temp_max":299.15},"wind":{"speed":2.6,"deg":310},"clouds":{"all":0},"dt":1469277000,"sys":{"type":1,"id":5641,"message":0.0032,"country":"FR","sunrise":1469248505,"sunset":1469303378},"id":2990969,"name":"Nantes","cod":200}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: initMenu
  • 32-36. satırlar: jSON yanıtları G/Ç iş parçacıklarından alınır
  • 37-41. satırlar: parça, UI iş parçacığından 5 yanıtı alır;

Şimdi, hatalı bir API kimliği ile isteği gerçekleştiriyoruz:


    String APIID = "";

Image

Bu durumda günlükler şu şekildedir:


07-23 13:34:43.853 11240-11240/client.android D/MeteoFragment_: beginWaiting
...
07-23 13:34:49.121 11240-11464/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.121 11240-11466/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11468/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11467/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Exception reçue
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Annulation des tâches lancées
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: initMenu
07-23 13:34:49.167 11240-11465/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
  • 3-6. ve 10. satırlar: 5 adet HTTP çağrısı 5 istisna oluşturdu;
  • 7. satır: [MeteoFragment] parçası ilk istisnayı alır. Bunun üzerine tüm görevleri iptal eder;

Şimdi [IMainActivity.DELAY] için 5 saniyelik bir bekleme süresi ayarlayalım ve işlemi iptal edelim. Bu durumda günlük kayıtları şu şekilde olur:


07-21 13:16:20.329 20390-20390/client.android D/MeteoFragment_: beginWaiting
...
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation demandée
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation des tâches lancées
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: initMenu
07-21 13:25:02.948 29965-30197/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30195/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30194/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30193/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30196/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
  • 3. satır: iptal talebi;
  • 4. satır: bir iptal gerçekleştiği için bekleme iptal edilir;
  • satır 6-10: görevlerin iptal edilmesi, beş görevin her bir iş parçacığında bir istisna oluşturur. İstisna türü uygulamalara bağlıdır. Buradaki istisna [java.lang.InterruptedException]'tir, çünkü görevler, kendilerini yapay olarak [delay] milisaniye bekletmeye zorlayan [Thread.sleep(delay)] komutunu yürütürken kesintiye uğramıştır;

2.8.3. Örnek-16B

Burada 1.17. paragrafındaki Örnek 16’yı yeniden düzenliyoruz. Bu örnek, bir rastgele sayı sunucusuna asenkron çağrılar yapan bir kod parçacığını göstermektedir. Aygıtın döndürülmesi sırasında nasıl davrandığını inceleyelim:

Image

  • [1]'te, aygıt iki kez döndürülür;

Image

Görüldüğü gibi tüm hata mesajları kaybolmuştur. Bunu düzeltmeye çalışacağız.

2.8.3.1. Örnek-16B projesi

[client-android-skel] projesini [exemples/Exemple-16B] projesine kopyalıyoruz, ardından yeni projeyi yüklüyoruz:

  

İlk proje olan [Exemple-16]'ten, [Exemple-16B]'e şu öğeleri kopyalıyoruz:

  • [res/layout/vue1.xml] dosyasını, [res/values] klasörünü:
  

[vue1.xml] görünümünün üst kenar boşluğunu 80 dp olarak değiştireceğiz:


  <TextView
    android:id="@+id/txt_Titre2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="80dp"
    android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
  • [Vue1Fragment] parçası:
  
  • [dao / service / Response] sınıfı:
  

Bu aşamada ilk derlemeyi deneyebiliriz:

  • İlk hata türü, imports ile ilgili olanlardır. [Exemple-16B]'e geçiş sırasında bazı sınıfların paketleri değişmiştir. Öncelikle bu tür hataları düzeltmeye başlıyoruz;
  • ikinci hata türü, [Vue1Fragment] sınıfında, bu sınıfın üst sınıfı olan [AbstractParent] tarafından zorunlu kılınan yöntemleri uygulamaması nedeniyle ortaya çıkmaktadır. Bu yöntemleri otomatik olarak oluştururuz;

İkinci bir derleme denemesi yapıyoruz:

  • kalan tüm hatalar artık en fazla değişiklik geçirecek olan [Vue1Fragment] sınıfında yoğunlaşmıştır;

2.8.3.2. [Vue1Fragment] parçası için bir durum oluşturuluyor

Parçadaki bazı bilgilerin, döndürme işleminden sonra parçayı döndürme öncesindeki haline geri yükleyebilmek için kaydedilmesi gerektiğini gördük. Bu nedenle, şimdilik boş bir [Vue1FragmentState] durumu oluşturuyoruz:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {

}

2.8.3.3. Projenin özelleştirilmesi

  

[IMainActivity] arayüzü, projenin belirli özelliklerini belirlemenize olanak tanır:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // oturuma giriş
  ISession getSession();

  // görünüm değişikliği
  void navigateToView(int position, ISession.Action action);

  // bekleme yönetimi
  void beginWaiting();

  void cancelWaiting();

  // uygulama sabitleri -------------------------------------

  // hata ayıklama modu
  boolean IS_DEBUG_ENABLED = true;

  // sunucu yanıtı için maksimum bekleme süresi
  int TIMEOUT = 1000;

  // istemci isteğinin yürütülmesinden önceki bekleme süresi
  int DELAY = 5000;

  // temel kimlik doğrulama
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // parçaların bitişikliği
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // sekme çubuğu
  boolean ARE_TABS_NEEDED = false;

  // bekleme görüntüsü
  boolean IS_WAITING_ICON_NEEDED = true;

  // uygulamanın parça sayısı
  int FRAGMENTS_COUNT = 1;

}
  • 25, 28, 31, 40. satırlar: katmanın özellikleri [DAO]. Temel kimlik doğrulaması gerekmez;
  • satır 34: parçaların bitişikliği. Burada tek bir parça olduğu için bu sabitin önemi yoktur;
  • satır 37: bu, sekmeli bir uygulama değildir;
  • satır 43: sadece bir parça vardır;

Parçaların durumunu saklayan [CoreState] sınıfı şu şekilde olacaktır:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
  // fragman ziyaret edildi mi, edilmedi mi
  protected boolean hasBeenVisited = false;
  // parçanın menüsünün durumu (varsa)
  protected MenuItemState[] menuOptionsState;

  // alıcı ve ayarlayıcılar
...
}
  • 12. satır: [Vue1Fragment] parçanın durum sınıfını tanımlıyoruz;

[Session] sınıfı şu şekildedir:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // jSON'te serileştirilemeyen öğeler @JsonIgnore ek etiketine sahip olmalıdır
}

Bu uygulamada parçalar arası iletişim olmadığı için bu sınıf boştur.

2.8.3.4. [DAO] katmanı

  

[DAO] katmanında üç sınıfın özelleştirilmesi gerekir:

  • IDao arayüzü;
  • Dao uygulaması;
  • web sunucusuyla iletişim kuran WebClient arayüzü / jSON;

[Response] sınıfı, onu kullanan [Exemple-16] projesinden gelmektedir:


package client.android.dao.service;

import java.util.List;

public class Response<T> {

    // ----------------- özellikler
    // işlem durumu
    private int status;
    // olası hata mesajları
    private List<String> messages;
    // yanıt gövdesi
    private T body;

    // oluşturucular
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // alıcı ve ayarlayıcılar
...
}

[WebClient] arayüzü şu şekilde olacaktır:


package client.android.dao.service;

import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // [a,b] aralığında 1 rastgele sayı
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);

}
  • 18-19. satırlar: rastgele sayı hizmetine ait URL. Bunun, müşterinin kök URL'sine (RestClientRootUrl, satır 12) ait olduğunu hatırlatırız. Burada bu kök URL, [http://localhost:8080] olacaktır;

[IDao] arayüzü şu şekilde olacaktır:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // Web hizmetinin URL'si
  void setUrlServiceWebJson(String url);

  // kullanıcı
  void setUser(String user, String mdp);

  // istemci zaman aşımı
  void setTimeout(int timeout);

  // temel kimlik doğrulama
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // hata ayıklama modu
  void setDebugMode(boolean isDebugEnabled);

  // istek öncesi istemci bekleme süresi (milisaniye cinsinden)
  void setDelay(int delay);

  // rastgele sayı hizmeti
  Observable<Response<Integer>> getAlea(int a, int b);

}
  • Hatırlatmak gerekirse, 6-22. satırlardaki yöntemler, [client-android-skel] projesinin IDao arayüzünde varsayılan olarak mevcuttur;
  • 25. satır: [getAlea] yöntemi, [a,b] aralığında bir rastgele sayı elde etmeyi sağlar. Bu sayı, [Response<Integer>] türündeki bir yanıtta elde edilir; bu türdeki yanıtta rastgele sayı, [body] alanında yer alır;

[IDao] arayüzü, aşağıdaki [Dao] sınıfı tarafından uygulanır:


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // web hizmeti istemcisi
  @RestService
  protected WebClient webClient;
  // güvenlik
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // RestTemplate
  private RestTemplate restTemplate;
  // RestTemplate fabrikası
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // günlük
    Log.d(className, "afterInject");
    // restTemplate üretiliyor
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // jSON dönüştürücüsünü sabitliyoruz
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // web istemcisinin restTemplate'i belirlenir
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // web hizmetinin URL'i belirlenir
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // kullanıcı, yakalayıcıya kaydedilir
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // fabrika yapılandırması
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // kimlik doğrulama yakalayıcısı mı?
    if (isBasicAuthentificationNeeded) {
      // kimlik doğrulama ara bileşeni ekleniyor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // özel yöntemler -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // rastgele sayı hizmeti
  @Override
  public Observable<Response<Integer>> getAlea(final int a, final int b) {
    // web istemcisi çalıştırma
    return getResponse(new IRequest<Response<Integer>>() {
      @Override
      public Response<Integer> getResponse() {
        return webClient.getAlea(a, b);
      }
    });
  }

}
  • 17-85 numaralı satırların, [client-android-skel] projesinin [Dao] sınıfında varsayılan olarak mevcut olduğu hatırlatılmalıdır. Sadece [IDao] arayüzünün uygulama yöntemlerini eklemek yeterlidir;
  • 88-97. satırlar: [getAlea] yönteminin uygulaması. Bu uygulama çok basittir ve 6 satırda gerçekleştirilmiştir (91-96. satırlar);
  • 91. satır: [getResponse] yöntemi, üst sınıf [AbstractDao]'e ait bir yöntemdir. Bu yöntem, T'nin beklenen yanıt türü olduğu (burada bir Response<Integer> türü) [IRequest<T>] türünde bir parametre bekler. [IRequest<T>]'in (satır 91) T türü, [Observable<T> getAlea] yönteminin (satır 89) T türü olmalıdır;
  • [IRequest<T>] arayüzünün tek bir yöntemi vardır: getResponse. Bu yöntemin görevi, [Observable<T> getAlea] yönteminin döndürmesi gereken T türündeki yanıtı sağlamaktır;
  • 94. satır: Bu yanıtı sağlayan arayüz [WebClient]'tir. Bu arayüze, 89. satırda alınan iki parametre aktarılır. Bu nedenle, bu parametrelerin final özniteliğine sahip olması gerekir;

2.8.3.5. [MainActivity] etkinliği

  

[MainActivity] etkinliği şu şekildedir:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // katman [DAO]
  @Bean(Dao.class)
  protected IDao dao;

  // üst sınıf yöntemleri -----------------------
  @Override
  protected void onCreateActivity() {
    // günlük
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // üst sınıf tarafından başlatılan başlatma işlemlerine devam ediliyor
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // parçaları burada tanımlayın
    return new AbstractFragment[]{new Vue1Fragment_()};
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // parça başlıklarını burada tanımlayın
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // sekmeli gezinme - görüntülenecek görünümü tanımlayın
  }

  @Override
  protected int getFirstView() {
    return 0;
  }

  // IDao arayüzü ------------------------------------------
  @Override
  public Observable<Response<Integer>> getAlea(int a, int b) {
    return dao.getAlea(a, b);
  }

}
  • [client-android-skel] projesinde 15-61. satırların varsayılan olarak mevcut olduğunu hatırlatmak isteriz. Bunları sadece özelleştirmeniz yeterlidir;
  • 40-44. satırlar: parça tablosu. Burada sadece bir tane var;
  • 47-51. satırlar: parça başlıkları gerekli değildir;
  • 53-56. satırlar: burada sekme yoktur;
  • 58-61. satırlar: Görüntülenecek ilk görünüm, [Vue1Fragment]'e ait 0 numaralı görünümdür;
  • satır 64-67: [IDao] arayüzünün uygulanması. Burada, işi 23. satırdaki [DAO] katmanına devretmekten başka yapılacak bir şey yoktur;

2.8.3.6. [Vue1Fragment] parçasının durumu

  

[Vue1FragmentState] sınıfı şu şekilde olacaktır:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

import java.util.ArrayList;
import java.util.List;

public class Vue1FragmentState extends CoreState {

  // parça durumu ------------------------
  // yanıt listesi
  private List<String> reponses = new ArrayList<>();
  // görünüm durumu ------------------------
  // istenen rastgele sayı sayısı ile ilgili hata mesajı
  private boolean txtErrorAleasVisible = false;
  // [a,b] aralığıyla ilgili hata mesajı
  private boolean txtErrorIntervalleVisible = false;
  // web hizmetindeki URL ile ilgili hata mesajı
  private boolean txtMsgErreurUrlServiceWebVisible = false;
  // bekleme süresi ile ilgili hata mesajı
  private boolean textViewErreurDelayVisible = false;
  // "Çalıştır" düğmesinin görünür olup olmadığı durumu
  private boolean btnExecuterVisible = true;

  // getter ve setter'lar
...
}

Parçada neyin kaydedilmesi gerektiğini belirlemek için, cihazı çeşitli durumlarda döndürdük ve geri yükleme sırasında nelerin kaybolduğuna baktık. Sonuç olarak, 10-23. satırlardaki bilgilerin kaydedilmesi gerektiği sonucuna vardık.

2.8.3.7. [Vue1Fragment] parçası

  

Şu anda [Vue1Fragment] görünümü, türediği üst sınıf [AbstractFragment]’in değişmiş olması nedeniyle çeşitli hatalar içeriyor. Yapılması gereken değişiklikleri tek tek açıklamaktansa, doğrudan son sürümü yorumlayacağız.

Parçanın iskeleti şu şekildedir:


package client.android.fragments.behavior;

import android.util.Log;
import android.view.View;
import android.widget.*;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.dao.service.Response;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.Observable;
import rx.functions.Action1;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vide)
public class Vue1Fragment extends AbstractFragment {

...
}
  • 26. satırda, her parçanın, boş da olsa bir menüye sahip olması gerektiği hatırlatılmaktadır. Burada da durum böyledir.

2.8.3.7.1. [Exécuter] düğmesine tıklama işleme

@Click(R.id.btn_Executer)
  protected void doExecuter() {
    // girdilen veriler kontrol ediliyor
    if (!isPageValid()) {
      return;
    }
    // önceki yanıtlar siliniyor
    reponses.clear();
    dataAdapterReponses.notifyDataSetChanged();
    // cevap sayacını 0'a sıfırlama
    nbReponses = 0;
    infoReponses.setText("Liste des réponses (0)");
    // etkinliğin başlatılması
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // rastgele görev hazırlanıyor
    beginWaiting(1);
    // rastgele sayılar isteniyor
    getAleasInBackground(nbAleas, a, b);
  }

  void getAleasInBackground(int nbAleas, int a, int b) {
    // gözlemlenecek süreç oluşturuluyor
    Observable<Response<Integer>> process = Observable.empty();
    for (int i = 0; i < nbAleas; i++) {
      process = process.mergeWith(mainActivity.getAlea(a, b));
    }
    // rastgele sayılar isteniyor
    executeInBackground(process, new Action1<Response<Integer>>() {

      @Override
      public void call(Response<Integer> response) {
        // yanıtı alınıyor
        consumeAleaResponse(response);
      }
    });
  }

  protected void consumeAleaResponse(Response<Integer> response) {
    // günlük
    if (isDebugEnabled) {
      try {
        Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // + yanıtı
    nbReponses++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
    // yanıt analiz ediliyor
    // hata mı?
    if (response.getStatus() != 0) {
      // görüntüleme
      showAlert(response.getMessages());
      // iptal
      doAnnuler();
      // kullanıcı arayüzüne geri dön
      return;
    }
    // bilgi yanıt listesine ekleniyor
    reponses.add(0, String.valueOf(response.getBody()));
    // yanıtlar yenileniyor
    dataAdapterReponses.notifyDataSetChanged();
  }

  // iptal ----------
  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asenkron görevleri iptal ediliyor
    cancelRunningTasks();
}

  private void beginWaiting(int nbRunningTasks) {
    // kum saati gösteriliyor
    beginRunningTasks(nbRunningTasks);
    // [Annuler] düğmesi, [Exécuter] düğmesinin yerini alıyor
    btnExecuter.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
  }
  • 4-6. satırlar: Öncelikle girdilerin geçerli olup olmadığı kontrol edilir. Bu sırada hata mesajları görüntülenebilir;
  • 8-9. satırlar: Yanıt listesi sıfırlanır. Bu değişiklik, yanıtları görüntüleyen ListView'e yansıtılır;
  • 11-12. satırlar: Alınan yanıt sayısı sıfırlanır;
  • satır 14: Rastgele sayı hizmetinin URL değeri belirlenir. Bu bilgi, [DAO] katmanına iletilecektir;
  • 15. satır: Rastgele sayı hizmetine istek gönderilmeden önce bekleme süresi belirlenir. Bu bilgi, [DAO] katmanına iletilecektir;
  • 17. satır: 1 asenkron görevi başlatmaya hazırlanılır (N değil, nedenini göreceğiz);
  • 24-27. satırlar: N adet asenkron görevden her biri, bir dizi işlemden oluşur [merge];
  • satır 29-36: üst sınıf [AbstractParent]'ten / jSON web hizmetine rastgele sayılar için sorgu yapması istenir;
  • satır 29-36: [executeInBackground] yöntemi iki parametre bekler:
    • 29. satır: gözlemlenecek ve yürütülecek işlem, önceki satırlarda hesaplanan işlemdir;
    • satır 29-36: asenkron hizmetten yanıt alındığında çalıştırılacak [Action1] örneği. [Action1<T>]'in T türü, [getAlea] yönteminin sonucunun T türü olmalıdır, yani bir [Response<Integer>] türü;
  • 34. satır: Bir yanıt geldiğinde (rastgele bir sayı), bu yanıt 39. satırdaki yöntemde işlenir;
  • 49-50. satırlar: yeni bir yanıt alındığı kaydedilir ve bildirilir;
  • 53-60. satırlar: [Response<T>] türünde, bir hata kodu olan [status] adlı bir alan bulunur. Bu kod sıfırdan farklıysa, sunucu bir sorunla karşılaşmıştır;
  • 55. satır: Bir hata mesajı görüntülenir. [showAlert] yöntemi üst sınıfa aittir;
  • 57. satır: 68-75. satırlardaki yöntem çağrılır. Bu yöntem, halen etkin olan görevleri iptal eder (74. satır);
  • satır 62: yanıt, ListView'in veri kaynağı olan yanıt listesine eklenir;
  • satır 64: ListView yenilenir;
  • 77-83. satırlar: [beginWaiting(int nbRunningTasks)] yöntemi, görünümü bekleme moduna hazırlar (81-82. satırlar) ve üst sınıfa [nbRunningTasks] görevlerinin yakında yürütüleceğini bildirir (79. satır);

2.8.3.7.2. Parçanın yaşam döngüsü

Parçanın yaşam döngüsü aşağıdaki yöntemler tarafından sağlanır:


  // yerel veriler
  private List<String> reponses;
  private ArrayAdapter<String> dataAdapterReponses;
  private int nbReponses = 0;
...
  // yaşam döngüsü yönetimi ---------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // görünümün mevcut durumu
    Vue1FragmentState state = new Vue1FragmentState();
    state.setTextViewErreurDelayVisible(textViewErreurDelay.getVisibility() == View.VISIBLE);
    state.setTxtErrorAleasVisible(txtErrorAleas.getVisibility() == View.VISIBLE);
    state.setTxtMsgErreurUrlServiceWebVisible(txtMsgErreurUrlServiceWeb.getVisibility() == View.VISIBLE);
    state.setTxtErrorIntervalleVisible(txtErrorIntervalle.getVisibility() == View.VISIBLE);
    state.setBtnExecuterVisible(btnExecuter.getVisibility() == View.VISIBLE);
    state.setReponses(reponses);
    return state;
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // İlk ziyaret mi?
    if (previousState != null) {
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      reponses = state.getReponses();
    } else {
      reponses = new ArrayList<>();
    }
    // listView veri kaynağı
    dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    // yanıt sayısı
    nbReponses = reponses.size();
  }

  @Override
  protected void initView(CoreState previousState) {
    // listview / adaptör bağlantısı
    listReponses.setAdapter(dataAdapterReponses);
    // İlk ziyaret mi?
    if (previousState == null) {
      // hata mesajları gizleniyor
      txtErrorAleas.setVisibility(View.INVISIBLE);
      txtErrorIntervalle.setVisibility(View.INVISIBLE);
      txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
      textViewErreurDelay.setVisibility(View.INVISIBLE);
      // düğmeler
      btnAnnuler.setVisibility(View.INVISIBLE);
      btnExecuter.setVisibility(View.VISIBLE);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // görünümün önceki durumu
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    // hata mesajlarını göster / gizle
    txtErrorAleas.setVisibility(state.isTxtErrorAleasVisible() ? View.VISIBLE : View.INVISIBLE);
    txtErrorIntervalle.setVisibility(state.isTxtErrorIntervalleVisible() ? View.VISIBLE : View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(state.isTxtMsgErreurUrlServiceWebVisible() ? View.VISIBLE : View.INVISIBLE);
    textViewErreurDelay.setVisibility(state.isTextViewErreurDelayVisible() ? View.VISIBLE : View.INVISIBLE);
    // düğmeler
    btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
    btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
    // yanıt sayısı
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // [Exécuter] düğmesi, [Annuler] düğmesinin yerini alır
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);

}
  • satır 7-18: üst sınıf talep ettiğinde parçanın kaydedilmesini sağlar;
  • 11. satır: bekleme süresi ile ilgili hata mesajının görüntülenmesi;
  • 12. satır: talep edilen rastgele sayıların sayısı ile ilgili hata mesajının görüntülenmesi;
  • 13. satır: web hizmeti / jSON'in URL'i ile ilgili hata mesajının görüntülenmesi;
  • satır 14: [a,b] rastgele sayı üretme aralığıyla ilgili hata mesajının görünürlüğü;
  • satır 15: [Exécuter] düğmesinin görünürlüğü;
  • 16. satır: alınan yanıtların listesi;
  • 20-23. satırlar: görünüm numarasını vermelidir. Burada parça numarası 0'dır, çünkü sadece bir tane vardır;
  • satır 25-38: parçanın alanlarının başlatılması; bu, ya ilk ziyaret (previousState==null) ya da sonraki bir ziyaret sırasında gerçekleşir;
    • 29-30. satırlar: İlk ziyaret değilse, [reponses] alanı parçanın önceki durumundan geri yüklenir;
    • satır 31-33: ilk ziyaretse, [reponses] alanı boş bir listeyle başlatılır;
    • satır 34-37: [reponses] alanından, parçanın ListView veri kaynağı (satır 35) ve yanıt sayısı (satır 37) oluşturulabilir;
  • satır 40-55: parçaya ilişkin görünümü başlatmak için yürütülür; bu işlem ya ilk ziyarette (previousState==null) ya da sonraki bir ziyarette gerçekleştirilir;
    • satır 43: parçanın ListView'i, [initFragment] yönteminde az önce oluşturulan veri kaynağına eşleştirilir;
    • satır 45-54: İlk ziyaret ise, görünüm ilk görüntülenmesi için hazırlanır;
  • satır 57-60: [SUBMIT] türündeki bir eylemle ilişkili parçalar arası gezinme sırasında yürütülür. Burada tek bir parça olduğu için parçalar arası gezinme söz konusu değildir;
  • 63-76. satırlar: [NAVIGATION] türünde bir eylemle ilişkili parçalar arası gezinme sırasında veya cihazın döndürülmesi ya da başka bir nedenden kaynaklanan bir yedekleme/geri yükleme döngüsü sırasında yürütülür. Burada yalnızca bu son durum gerçekleşebilir. Unutulmamalıdır ki, burada her durumda [previousState] her zaman null değildir;
  • 65. satır: önceki durum, parça durumunun türüne dönüştürülür;
  • 66-75. satırlar: görünümü geri yüklemek için önceki durumun içeriği kullanılır;
  • 78-81. satırlar: önceki tüm güncellemeler tamamlandığında çağrılır. Burada yapılacak bir şey yoktur;
  • satır 83-89: tüm asenkron görevler tamamlandığında yürütülür. Burada [Annuler] düğmesi gizlenir ve yerine [Exécuter] düğmesi getirilir;

2.8.3.8. Testler

Okuyucunun aşağıdaki testleri yapması önerilir:

  • hatalar oluşturup aygıtı çalıştırın: hata mesajları ekranda kalmalıdır;
  • rastgele sayılar elde edin ve cihazı çalıştırın: elde edilen rastgele sayılar ekranda kalmalıdır;
  • birkaç saniyelik bir bekleme süresi ayarlayın ve bekleme süresince aygıtı çalıştırın: görevler iptal edilmiş olmalıdır (bu, günlüklerde görülebilir);

2.8.4. Örnek-22B

Burada, örnek 22'yi [client-android-skel] proje modeline göre yeniden düzenlemek üzere ele alıyoruz. [Exemple-22] projesinin, döndürme sırasında parçaların yedekleme/geri yükleme döngüsünü doğru bir şekilde yönettiğini ve [client-android-skel] projesinin temelini oluşturan projenin bu olduğunu hatırlatırız.

[client-android-skel] projesini [exemples/Exemple-22B] olarak kopyalıyoruz ve bu son projeyi yükliyoruz:

  

Ardından, [Exemple-22] projesindeki çeşitli öğeleri [Exemple-22B] projesine kopyalıyoruz.

Öncelikle, [res] klasöründeki öğeleri kopyalıyoruz:

  • [layout/fragment_main.xml, layout/vue1.xml, menu/menu_fragment.xml, menu/menu_main.xml, [values] klasörü;
  

İki görünümün üst kenar boşluğunu 120 dp olarak değiştireceğiz:

[vue1.xml]:


  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="@string/titre_vue1"
    android:id="@+id/textViewTitreVue1"
    android:layout_marginTop="120dp"
    android:textSize="50sp"
    android:layout_gravity="center|left"
    android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>

[fragment_main]:


  <TextView
    android:id="@+id/section_label"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
android:layout_marginTop="120dp"/>

Ardından [Vue1Fragment, PlaceHolderFragment, PlaceHolderFragmentState] öğelerini kopyalayacağız:

 

Bu aşamada ilk derlemeyi deneyebiliriz. İlk hata türü ortaya çıkar: sınıfların paketleri değiştiği için hatalı olan imports öğeleri. Bu imports öğelerini düzeltiriz. İkinci tür hatalar ise, parçaların üst sınıfları olan [AbstractFragment]'in tüm yöntemlerini uygulamamalarından kaynaklanmaktadır. Bunları (Alt+Enter) tuşlarıyla düzeltiriz.

Kalan hatalar, eski ve yeni sınıflar arasındaki mevcut farklılıklardan kaynaklanmaktadır ([AbstractFragment]). Şu an için bunları göz ardı ediyoruz.

2.8.4.1. Projenin özelleştirilmesi

  

[custom] klasöründe, geliştirici tarafından özelleştirilebilen mimari öğeler bulunur.

[IMainActivity] arayüzü, projenin belirli özelliklerini belirlemenizi sağlar:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // oturuma erişim
  ISession getSession();

  // görünüm değiştirme
  void navigateToView(int position, ISession.Action action);

  // bekleme yönetimi
  void beginWaiting();

  void cancelWaiting();

  // hata ayıklama modu
  boolean IS_DEBUG_ENABLED = true;

  // sunucu yanıtı için maksimum bekleme süresi
  int TIMEOUT = 1000;

  // istemci isteğinin yürütülmesinden önceki bekleme süresi
  int DELAY = 0;

  // temel kimlik doğrulama
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // parçaların bitişikliği
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // sekme çubuğu
  boolean ARE_TABS_NEEDED = true;

  // bekleme görüntüsü
  boolean IS_WAITING_ICON_NEEDED = false;

  // parça sayısı
  int FRAGMENTS_COUNT = 5;

}
  • 23, 26, 29, 38. satırlar: [DAO] katmanının özellikleri. Burada herhangi bir özellik yoktur;
  • satır 41: burada beş parça vardır;
  • satır 32: parçaların bitişikliği. Bu sabit, burada [1,4]'te bir değere sahip olabilir. Okuyucunun, uygulamanın çalışmaya devam edip etmediğini görmek için bu değeri değiştirmesi önerilir;
  • satır 35: bu, sekmeli bir uygulamadır;

Parçaların durumunu saklayan [CoreState] sınıfı şu şekilde olacaktır:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.PlaceHolderFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = PlaceHolderFragmentState.class)}
)
public class CoreState {
  // ziyaret edilmiş veya edilmemiş parça
  protected boolean hasBeenVisited = false;
  // parçanın menüsünün durumu (varsa)
  protected MenuItemState[] menuOptionsState;

  // alıcı ve ayarlayıcılar
...
}
  • 12. satır: [PlaceHolderFragment] parçacık durum sınıfını tanımlıyoruz. [Vue1Fragment] parçacığının ise bir durumu yoktur;

[Session] sınıfı şu şekildedir:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // parçalar arasında ve parçalar ile etkinlik arasında paylaşılacak veriler
  // jSON formatında serileştirilemeyen öğeler @JsonIgnore ek etiketine sahip olmalıdır
  // jSON formatında serileştirme/deserileştirme için gerekli getter ve setter yöntemlerini unutmayın

  // ziyaret edilen parça sayısı
  private int numVisit;
  // ikinci sekmede görüntülenen [PlaceholderFragment] türündeki parça numarası
  private int numFragment = -1;

  // getter ve setter'lar
...
}

Bu, [Exemple-22] projesinin oturumudur.

2.8.4.2. [MainActivity] etkinliği

  

[MainActivity] etkinliği şöyledir:


package client.android.activity;

import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.util.Log;
import android.view.MenuItem;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.PlaceholderFragment_;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // [DAO] katmanı
  @Bean(Dao.class)
  protected IDao dao;
  // oturum
  private Session session;

  // menü yönetimi-----------------------
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
...
  }

  private void showFragment(int i) {
...
  }

  // üst sınıf yöntemlerinin uygulanması ---------------------------------------------------
  ...
}

Burada, [MainActivity] sınıfı, iki nedenden dolayı önceki örneklerdekinden daha kapsamlıdır:

  • yönetilmesi gereken sekmeler vardır;
  • yönetilmesi gereken bir menü vardır;

2.8.4.2.1. Üst sınıfın yöntemlerinin uygulanması

// üst sınıf yöntemleri -----------------------
  @Override
  protected void onCreateActivity() {
    // günlük
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // üst sınıf tarafından başlatılan başlatma işlemlerine devam ediyoruz
    // oturum
    this.session = (Session) super.session;
    ...
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // parça numarası
    final String ARG_SECTION_NUMBER = "section_number";
    // parça dizisinin başlatılması
    AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
    int i;
    for (i = 0; i < fragments.length - 1; i++) {
      // bir parça oluşturuluyor
      fragments[i] = new PlaceholderFragment_();
      // parçaya argümanlar aktarılabilir
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, i + 1);
      fragments[i].setArguments(args);
    }
    // bir + parçası
    fragments[i] = new Vue1Fragment_();
    // sonuç
    return fragments;
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // burada başlık yok
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
...
  }

  @Override
  protected int getFirstView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }
  • 2-12. satırlar: [onCreateActivity] yöntemi, etkinlik ilk kez oluşturulduğunda veya bir kaydetme/geri yükleme döngüsü sırasında yeniden oluşturulduğunda üst sınıf [AbstractActivity] tarafından çağrılır. Bu yöntem çağrıldığında, üst sınıf oturumu çoktan geri yüklemiştir;
  • 10. satır: Oturumun yerel bir referansı alınır. Tip değişikliği, üst sınıfın oturumunun [AbstractSession] tipi olmasından kaynaklanmaktadır;
  • satır 19-38: [getFragments] yöntemi, uygulama tarafından yönetilen parça dizisini üst sınıfa geri vermelidir. Burada [FRAGMENTS_COUNT] adet parça vardır; bu sayı [IMainActivity]'te tanımlanmıştır. İlk [FRAGMENTS_COUNT-1] parçaları [PlaceHolderFragment] türündedir ve sonuncusu [Vue1Fragment] türündedir;
  • 41-45. satırlar: [getFragmentTitle] yöntemi, bu bilgi yararlı olabileceği durumlarda parçaların başlıklarını döndürmelidir. Burada durum böyle değildir;
  • 47-50. satırlar: Bu yöntem, kullanıcı bir sekmeye tıkladığında üst sınıf tarafından çağrılır. Buna bir sonraki paragrafta tekrar değineceğiz;
  • satır 52-55: Uygulama başlatıldığında görüntülenecek ilk görünümün numarasını döndürür. Burada ilk olarak [Vue1Fragment] parçası görüntülenmelidir. [getFirstView] yöntemi, [IMainActivity] içinde bir sabit ile değiştirilebilir;

2.8.4.2.2. Sekme Yönetimi

Sekmeler aşağıdaki yöntemlerle yönetilir:


@Override
  protected void onCreateActivity() {
    // günlük
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // üst sınıf tarafından başlatılan başlatma işlemleri devam eder
    // oturum
    this.session = (Session) super.session;
    // 1. sekme
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
    // 2. sekme?
    int numFragment = session.getNumFragment();
    if (numFragment != -1) {
      TabLayout.Tab tab2 = tabLayout.newTab();
      tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
      tabLayout.addTab(tab2);
    }
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // görüntülenecek parçanın numarası
    int numFragment;
    switch (position) {
      case 0:
        // parça numarası [Vue1Fragment]
        numFragment = getFirstView();
        break;
      default:
        // parça numarası [PlaceholderFragment]
        numFragment = session.getNumFragment();
    }
    // parça görüntüsü
    if (numFragment != mViewPager.getCurrentItem()) {
      navigateToView(numFragment, ISession.Action.SUBMIT);
    }
  }
}
  • 1-20. satırlar: [onCreateActivity] yöntemi, etkinlik ilk kez oluşturulduğunda veya bir kaydetme/geri yükleme döngüsü sırasında yeniden oluşturulduğunda üst sınıf [AbstractActivity] tarafından çağrılır. Bu yöntem çağrıldığında, üst sınıf oturumu zaten geri yüklemiştir;
  • 9. satır: Oturumun yerel bir referansı alınır. Tip değişikliği, üst sınıfın oturumunun [AbstractSession] tipi olmasından kaynaklanmaktadır;
  • satır 11-13: ilk sekme oluşturulur;
  • satır 15-20: oturumda bir parça numarası kayıtlıysa (satır 15) ikinci sekme oluşturulur. Bu numara, etkinliğin ilk oluşturulmasında başlangıçta -1 değerindedir;
  • satır 23-39: kullanıcı bir sekmeye tıkladığında bu yöntem üst sınıf tarafından çağrılır;
  • 28-31. satırlar: 0 numaralı sekme tıklanırsa, [Vue1Fragment] görüntülenmelidir. Bunun, uygulamanın başlatılması sırasında görüntülenen ilk görünüm olduğu bilinmektedir;
  • satır 32-35: 1 numaralı sekme tıklanırsa, oturumda numarası kaydedilmiş olan parçanın görüntülenmesi gerekir;
  • satır 37-39: Seçilen parçaya yöneliniyoruz. İlişkili eylem [SUBMIT]'tir. Bu, [NAVIGATION] olabilirdi mi? Bu belgede, [NAVIGATION] yalnızca yeni parçanın görüntülenmesi için önceki durumunun bilinmesi gerektiğinde kullanılır. Burada durum böyle değildir, çünkü görüntülenen parçanın önceki durumuna göre değişmesi ve bir ziyaret daha gösterilmesi gerekir;

2.8.4.2.3. Menü yönetimi

Etkinlik, aşağıdaki [menu_main.xml] menüsüyle ilişkilidir:


<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context="exemples.android.MainActivity">
  <item android:id="@+id/action_settings"
        android:title="@string/action_settings"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment1"
        android:title="@string/fragment1"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment2"
        android:title="@string/fragment2"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment3"
        android:title="@string/fragment3"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment4"
        android:title="@string/fragment4"
        android:orderInCategory="100"
        app:showAsAction="never"/>
</menu>

ve şu içeriği görüntüler:

  

Menü yönetimi aşağıdaki yöntemlerle gerçekleştirilir:


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // günlük
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onOptionsItemSelected");
    }
    // menü seçeneklerinin işlenmesi
    int id = item.getItemId();
    switch (id) {
      case R.id.action_settings: {
        if (IS_DEBUG_ENABLED) {
          Log.d(className, "action_settings selected");
        }
        break;
      }
      case R.id.fragment1: {
        showFragment(0);
        break;
      }
      case R.id.fragment2: {
        showFragment(1);
        break;
      }
      case R.id.fragment3: {
        showFragment(2);
        break;
      }
      case R.id.fragment4: {
        showFragment(3);
        break;
      }
    }
    // işlenen öğe
    return true;
  }

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // seçilen sekmede yazılımla gezinme yok
      session.setNavigationOnTabSelectionNeeded(false);
      // başlıkların yazı tipi sorunları nedeniyle iki sekme yeniden oluşturuluyor
      tabLayout.removeAllTabs();
      tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
      tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
      // görüntülenecek parçanın numarası oturuma kaydedilir
      session.setNumFragment(i);
      // 2 numaralı sekme, gezinme özelliği ile seçiliyor
      session.setNavigationOnTabSelectionNeeded(true);
      tabLayout.getTabAt(1).select();
    }
  }
  • 16-31. satırlar: [Fragmenti] türündeki bir menü seçeneğine tıklama yönetimi;
  • satır 37-50: i numaralı parçayı (PlaceHolderFragment türündeki parçalar) 1 numaralı sekmede (2. sekme) görüntüler;
  • satır 42-44: mevcut sekmeleri silip iki yeni sekme oluşturmaya karar verilir. Bu karar, şu sorunu aşmak için alınmıştır: parçayı mevcut 1 numaralı sekmede (yani silmeden) görüntülemekle yetindiğimizde, garip bir şekilde başlığı, 0 numaralı sekmenin başlığından farklı bir görünüme (yazı tipi, boyut) sahiptir;
  • satır 43-44: iki sekme oluşturulur ancak seçilmez (false'teki son parametre);
  • 40. satır: 42-44. satırlardaki işlemler, sekmeler üzerinde [select] işlemlerini gerçekleştirebilir ve bu da [onTabSelected] yöneticisini çağırır. Herhangi bir işlem yapılmazsa, bir parçaya yönlendirme gerçekleşir. Bunu önlemek için oturumda [navigationOnTabSelectionNeeded] boole değerini faux olarak ayarlayabiliriz. Bir fragman görünür hale geldiğinde, bu boole değeri [AbstractFragment] sınıfı tarafından otomatik olarak vrai olarak sıfırlanır;
  • 46. satır: Oturumda görüntülenecek parçanın numarası kaydedilir;
  • 48-50. satırlar: gezinme özelliğine sahip 2 numaralı sekme seçilir (48. satır). Bu, [onTabSelected] prosedürünü tetikler ve bu prosedür:
    • oturuma kaydedilen numaralı parçayı görüntüler;
    • seçilen sekmenin numarasını oturumda kaydeder;

2.8.4.3. [Vue1Fragment] parçası

Burada parçanın son halini veriyoruz:


package client.android.fragments.behavior;

import android.widget.EditText;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;

@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class Vue1Fragment extends AbstractFragment {

  // görsel arayüz öğeleri
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

  // olay yöneticisi
  @Click(R.id.buttonValider)
  protected void doValider() {
    // girilen ad görüntülenir
    Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

  // parçanın yaşam döngüsü -----------------------------------------------
  private void initFragment() {
    // yapılacak bir şey yok
  }

  // parça durumunu kaydet
  @Override
  public CoreState saveFragment() {
    // görünüm durumu - kaydedilecek bir şey yok
    return new CoreState();
  }

  @Override
  protected int getNumView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // yapılacak bir şey yok
  }

  @Override
  protected void initView(CoreState previousState) {
    // İlk ziyaret mi?
    if (previousState == null) {
      // ziyaret numarası görüntülenir
      showNumVisit();
    }

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // ziyaret numarası görüntüleniyor
    showNumVisit();

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

  // özel yöntemler -------------------------------------
  // ziyaret numarasını gösterme
  private void showNumVisit() {
    // ziyaret numarasını artır
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // ziyaret numarası görüntülenir
    Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
  }
}

Sınıf neredeyse boştur.

  • 35-39. satırlar: Parça durumunu kaydetmesi gerektiğinde üst sınıf tarafından çağrılır. [Vue1Fragment] parçacığının kaydedilecek bir durumu yoktur. Basitçe temel sınıf [CoreState]'in bir örneğini döndürürüz (hatırlatma: null'i döndürmemeliyiz);
  • satır 41-44: parçanın numarasını döndürmelidir. [Vue1Fragment] parçası, yapısı gereği [FRAGMENTS_COUNT-1] numarasına sahiptir;
  • satır 51-59: parça ilk kez oluşturulduğunda (previousState==null) veya sonraki seferlerde (previousState!=null) üst sınıf tarafından çağrılır;
    • satır 54-57: ilk ziyaret ise, ziyaret numarası artırılır ve görüntülenir (satır 85-92);
  • satır 61-65: Parça, [SUBMIT] eylemiyle ilişkili olarak görüntülenecekken çağrılır. Ziyaret numarası artırılır ve görüntülenir. Burada, yaşam döngüsü boyunca ziyaret numarasının iki kez artırılması mümkün değildir. Nitekim, [Vue1Fragment] fragmanına ilk ziyaret, oturumun yapısı gereği eylemin [NONE] olduğu uygulama başlangıcında gerçekleşir. Bu, [updateOnSubmit] yönteminin çağrılmayacağını garanti eder. Bundan sonra, bu artık ilk ziyaret olmayacak ve [initView] yöntemi hiçbir işlem yapmayacaktır;
  • 68-71. satırlar: yedekleme/geri yükleme döngüsü içinde çağrılır. Parçanın bir durumu olmadığı için burada geri yüklenecek bir şey yoktur;
  • 73-76. satırlar: önceki tüm güncellemeler tamamlandığında çağrılır. Burada yapılacak başka bir şey yoktur;
  • 78-81. satırlar: başlatılan asenkron görevlerin tümü tamamlandığında çağrılır. Burada asenkron görev yoktur;

2.8.4.4. [PlaceHolderFragmentState] durumu

[PlaceHolderFragment] parçacığının durumu şu şekilde olacaktır:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class PlaceHolderFragmentState extends CoreState {
  // metin
  private String text;

  // yapıcılar
  public PlaceHolderFragmentState() {

  }

  public PlaceHolderFragmentState(String text) {
    super();
    this.text = text;
  }

  // alıcı ve ayarlayıcılar
 ...
}
  • parçanın durumu kaydedilmesi gerektiğinde, parçanın görüntülediği metin (7. satır) kaydedilecektir;

2.8.4.5. [PlaceHolderFragment] parçası

[PlaceHolderFragment] parçası şu şekilde olacaktır:


package client.android.fragments.behavior;

import android.util.Log;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.PlaceHolderFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;

@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {

  // görsel arayüz bileşenleri
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  @ViewById(R.id.textView1)
  protected TextView textView1;

  // veriler
  private String text;

  // parça numarası
  private static final String ARG_SECTION_NUMBER = "section_number";

  // üst sınıf yöntemlerinin uygulanması ----------------------------
  @Override
  public CoreState saveFragment() {
    // parça durumunun kaydedilmesi
    PlaceHolderFragmentState placeHolderFragmentState = new PlaceHolderFragmentState();
    placeHolderFragmentState.setText(textViewInfo.getText().toString());
    return placeHolderFragmentState;
  }

  @Override
  protected int getNumView() {
    return getArguments().getInt(ARG_SECTION_NUMBER) - 1;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // orijinal metin
    text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
  }

  @Override
  protected void initView(CoreState previousState) {
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // görüntülenen metin güncelleniyor
    // ziyaret sayısını artırma
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // değiştirilmiş metin
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
    // günlük
    if (isDebugEnabled) {
      Log.d(className, String.format("updateForSubmit, numvisit=%s, texte affiché=%s, visibility=%s", numVisit, textViewInfo.getText().toString(), textViewInfo.getVisibility()));
    }
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // görüntülenen metin geri yükleniyor
    PlaceHolderFragmentState state = (PlaceHolderFragmentState) previousState;
    textViewInfo.setText(state.getText());

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

}
  • 30-36. satırlar: üst sınıf, parçaya durumunu kaydetmesini istediğinde, parça tarafından görüntülenen metin kaydedilir (34. satır);
  • 38-41. satırlar: parçanın numarasını döndürür. Bu numara, parçanın oluşturulurken argüman olarak geçirilen bölüm numarasına bağlıdır;
  • 43-47. satırlar: parçanın ilk oluşturulmasında (previousState==null) veya sonraki oluşturulmalarda (previousState !=null) çağrılır;
    • 46. satır: Burada önceki durum kullanılmamaktadır. İlk ziyaret sırasında görüntülenen başlangıç metni [text] (24. satır) her seferinde yeniden hesaplanmaktadır. Bu tartışmaya açıktır. Bu bilgiyi de parça durumuna eklemek tercih edilebilirdi;
  • satır 49-51: Parçayla ilişkili görünümün ilk oluşturulmasında (previousState==null) veya sonraki oluşturulmalarda (previousState!=null) çağrılır. Yapılacak bir şey yoktur;
  • satır 53-56: Parça, [SUBMIT] eylemiyle ilişkili olarak görüntülenecekken çağrılır. Eylemin [RESTORE] olduğu yedekleme/geri yükleme döngüsü hariç, bu her zaman geçerlidir. Dolayısıyla ziyaret numarasını artırır ve görüntüleriz;
  • satır 68-74: bir yedekleme/geri yükleme döngüsünde çağrılır. Parçanın durumuna yedeklenmiş olan metin geri yüklenir;
  • satır 76-79: önceki tüm güncellemeler tamamlandığında çağrılır. Burada yapılacak başka bir işlem yoktur;
  • satır 82-83: başlatılan asenkron görevlerin tümü tamamlandığında çağrılır. Burada asenkron görev yoktur;

2.8.4.6. Tests

Okuyucunun, cihazı döndürerek uygulamayı test etmesi ve görüntülenen parçacığın durumunu kaybetmediğini doğrulaması önerilir. Ayrıca günlükler de incelenmelidir.

2.9. Conclusion

Bu bölümün sonunda, bir web hizmeti / jSON ile iletişim kuran ve aşağıdaki özelliklere sahip bir Android istemci model projesi [client-android-skel] elimizde bulunmaktadır:

  • web sunucusu / jSON ile asenkron iletişim, RxJava kütüphanesi kullanılarak sağlanır;
  • bir parçanın yaşam döngüsü (güncelleme, kaydetme, geri yükleme), belirli zamanlarda alt sınıflarının belirli yöntemlerini çağıran üst sınıfı [AbstractFragment] tarafından yönetilir. Böylece alt parça, yaşam döngüsünün aşamalarıyla ilgilenmek zorunda kalmaz, yalnızca üst sınıfı tarafından zorunlu kılınan belirli yöntemleri uygulamakla yetinir;
  • etkinliğin yaşam döngüsü (kaydetme / geri yükleme), yine alt etkinliğe belirli yöntemleri uygulamayı zorunlu kılan [AbstractActivity] adlı soyut sınıf tarafından yönetilir;
  • [AbstractActivity] sınıfı, sekmeli veya sekmesiz, bekleme görüntüsü olan veya olmayan, web sunucusu / jSON üzerinden temel kimlik doğrulaması olan veya olmayan bir uygulamayı yönetebilir. Bu öğelerin varlığı veya yokluğu yapılandırma yoluyla belirlenir;

Şimdi, önceki örneklerden daha karmaşık bir vaka çalışması sunacağız. Yeni uygulama, [client-android-skel] model projesini temel alacaktır.