非同期操作とAndroidでのアクティビティの再作成

ハブに関する1つの記事( 274635 )で、オブジェクトをonRestoreInstanceState



せずにonRestoreInstanceState



からonSaveInstanceState



に転送するための奇妙なソリューションが示されました。 android.os.Parcel



クラスのwriteStrongBinder(IBInder)



メソッドを使用します。



このようなソリューションは、Androidがアプリケーションをアンロードするまで正しく機能します。 そして、彼にはそれを行う権利があります。

...システムは、他のフォアグラウンドまたは可視プロセスのメモリを再利用するために、そのプロセスを安全に終了する場合があります...

http://developer.android.com/intl/en/reference/android/app/Activity.html





ただし、これはポイントではありません。 (このような再起動後にアプリケーションがその状態を復元する必要がない場合、このソリューションも適しています)。



しかし、そのような「シリアライズ不可能な」オブジェクトが使用される目的は、私には奇妙に思えました。 そこで、アプリケーションの表示状態を更新するために、非同期操作からActivity



に呼び出しを転送します。



私はいつも、Smalltalk以来、どんな開発者もこの典型的な設計作業を認識すると思っていました。 しかし、私は間違っていたようです。



挑戦する





特徴



後者の場合、次回Activity



開いたときに結果が表示されます。



解決策



MVC(アクティブモデル付き)およびレイヤー。



詳細なソリューション



記事の残りの部分では、MVCとレイヤーについて説明します。



具体例を挙げて説明します。 アプリケーション「電子キューへの電子チケット」を作成する必要があります。

  1. ユーザーは銀行支店に入り、アプリケーションの「チケットを取得」ボタンをクリックします。 アプリケーションはサーバーにリクエストを送信し、チケットを受信します。
  2. キューが開くと、アプリケーションは連絡する必要があるウィンドウの番号を表示します。


非同期操作を使用してサーバーからチケットを受け取ります。 また、非同期操作はファイルからチケットを読み取り(再起動後)、ファイルを削除します。



このようなアプリケーションは、単純なコンポーネントから構築できます。 例:

  1. チケットが配置されるコンポーネント( TicketSubsystem



  2. チケットが表示されるTicketActivity



    および「チケットをTicketActivity



    」ボタン
  3. チケットのクラス(チケット番号、広告申込情報、ウィンドウ番号)
  4. 非同期チケット取得のクラス


最も興味深いのは、これらのコンポーネントがどのように相互作用するかです。



アプリケーションには、 TicketSubsystem



コンポーネントを含める必要はありません。
チケットは

静的フィールドTicket.currentTicket



、またはTicket.currentTicket



クラスandroid.app.Application



フィールド。


ただし、その条件は、ロールを実行できるオブジェクトからのチケットである/ないことであることが非常に重要です

MVC







-つまり、チケットが表示された(または置き換えられた)ときに通知を生成します。




TicketSubsystem



MVC



観点からモデルにすると、 Activity



はイベントをサブスクライブし、ロード時にチケット表示を更新できます。 この場合、 Activity



MVC



観点からView



として機能しView







その後、非同期操作「新しいチケットを取得」は、受信したチケットをTicketSubsystem



記録するだけで、他のことを心配する必要はありません。



モデル



明らかに、チケットはモデルでなければなりません。 ただし、アプリケーションでは、チケットを空中に「吊るす」ことはできません。 さらに、チケットは最初は存在せず、非同期操作の完了時にのみ表示されます。 このことから、チケットが配置されるアプリケーションには何か他のものがなければならないことがわかります。 それをTicketSubsystem



ます。 チケット自体も何らかの方法で提示する必要があり、それをTicket



クラスにします。 これらのクラスは両方とも、アクティブなモデルの役割を果たすことができる必要があります。



アクティブなモデルを構築する方法



アクティブなモデル-モデルは、変更が発生したというアイデアを通知します。 ウィキペディア



アクティブなモデルを作成するためのjavaのヘルパークラスがいくつかあります。 以下に例を示します。

  1. java.beans



    パッケージのPropertyChangeSupport



    およびPropertyChangeListener



  2. java.util



    パッケージのObservable



    およびObserver



  3. android.databinding



    からのBaseObservable



    およびObservable.OnPropertyChangedCallback





私は個人的に第三の方法が好きです。 android.databinding.Bindable



アノテーションのおかげで、観測フィールドの厳密な命名をサポートします。 しかし、他の方法があり、それらはすべて適切です。



Groovyには素晴らしい注釈groovy.beans.Bindableがあります。 オブジェクトのプロパティを簡単に宣言する機能とともに、非常に簡潔なコードが取得されます(これはjava.beans



PropertyChangeSupport



依存していjava.beans



)。



 @groovy.beans.Bindable class TicketSubsystem { Ticket ticket } @groovy.beans.Bindable class Ticket { String number int positionInQueue String tellerNumber }
      
      





提出



TicketActivity



(プレゼンテーションに関連するほとんどすべてのオブジェクトと同様)は、ユーザーの意志で表示および非表示になります。 アプリケーションは、 Activity



が表示され、データが変更されたときにActivity



が表示されるときにのみ、データを正しく表示する必要があります。



そのため、 TicketActivity



次が必要です。

  1. ticket



    データを変更するときにUIウィジェットを更新する
  2. リスナーが表示されたらチケットに接続します
  3. リスナーをTicketSubsytem



    接続します( ticket



    が表示されたときにビューを更新します)


1. UIウィジェットを更新します。



この記事の例では、デモンストレーションのためにjava.beans



PropertyChangeListener



を使用します


詳細。 また、ソースコードでは、記事の下部にあるリンクでandroid.databinding



ライブラリを使用しますが、


最も簡潔なコードを提供するものとして。



 PropertyChangeListener ticketListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { updateTicketView(); } }; void updateTicketView() { TextView queuePositionView = (TextView) findViewById(R.id.textQueuePosition); queuePositionView.setText(ticket != null ? "" + ticket.getQueuePosition() : ""); ... }
      
      





2.リスナーをチケットに接続する





 PropertyChangeListener ticketSubsystemListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { setTicket(ticketSubsystem.getTicket()); } }; void setTicket(Ticket newTicket) { if(ticket != null) { ticket.removePropertyChangeListener(ticketListener); } ticket = newTicket; if(ticket != null) { ticket.addPropertyChangeListener(ticketListener); } updateTicketView(); }
      
      





チケットを変更すると、 setTicket



メソッドは古いチケットからイベントサブスクリプションを削除し、新しいチケットからイベントをサブスクライブします。 setTicket(null)



を呼び出すと、 TicketActivity



ticket



イベントのサブスクTicketActivity



解除します。



3.リスナーをTicketSubsystemに接続する





 void setTicketSubsystem(TicketSubsystem newTicketSubsystem) { if(ticketSubsystem != null) { ticketSubsystem.removePropertyChangeListener(ticketSubsystemListener); setTicket(null); } ticketSubsystem = newTicketSubsystem; if(ticketSubsystem != null) { ticketSubsystem.addPropertyChangeListener(ticketSubsystemListener); setTicket(ticketSubsystem.getTicket()); } } @Override protected void onPostCreate(@Nullable Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); setTicketSubsystem(globalTicketSubsystem); } @Override protected void onStop() { super.onStop(); setTicketSubsystem(null); }
      
      





コードは非常に簡単です。 しかし、特別なツールを使用せずに、同じタイプの操作を非常に多く記述する必要があります。 モデル階層内の各要素について、フィールドを作成し、個別のリスナーを作成する必要があります。



非同期操作「チケットを取る」



非同期操作コードも非常に簡単です。 主なアイデアは、非同期操作の完了時に結果を



に書き込むことです。 そして、







からの通知によって更新されます。



 public class GetNewTicket extends AsyncTask<Void, Void, Void> { private int queuePosition; private String ticketNumber; @Override protected Void doInBackground(Void... params) { SystemClock.sleep(TimeUnit.SECONDS.toMillis(2)); Random random = new Random(); queuePosition = random.nextInt(100); ticketNumber = "A" + queuePosition; // TODO     ,    //     . return null; } @Override protected void onPostExecute(Void aVoid) { Ticket ticket = new Ticket(); ticket.setNumber(ticketNumber); ticket.setQueuePosition(queuePosition); globalTicketSubsystem.setTicket(ticket); } }
      
      





ここで、 globalTicketSubsystem



リンク( globalTicketSubsystem



も記載されていTicketActivity



)は、アプリケーションでサブシステムをTicketActivity



方法によって異なります。



再起動時に状態を復元する



ユーザーが「Take a Ticket」ボタンをクリックし、アプリケーションがサーバーにリクエストを送信し、その時点で着信コールが発生したとします。 ユーザーが呼び出しに応答している間に、サーバーから応答がありましたが、ユーザーはそれについて知りません。 さらに、ユーザーは「ホーム」をクリックして、すべてのメモリを食い尽くすアプリケーションを起動し、システムはアプリケーションをアンロードする必要がありました。



これで、アプリケーションは再起動前に受け取ったチケットを表示するはずです。



この機能を提供するには、チケットをファイルに書き込み、アプリケーションの起動後に読み取ります。



 public class ReadTicketFromFileextends AsyncTask<File, Void, Void> { ... @Override protected Void doInBackground(File... files) { //     number, positionInQueue, tellerNumber } @Override protected void onPostExecute(Void aVoid) { Ticket ticket = new Ticket(); ticket.setNumber(number); ticket.setPositionInQueue(positionInQueue); ticket.setTellerNumber(tellerNumber); globalTicketSubsystem.setTicket(ticket); } }
      
      





レイヤー



このテンプレートは、1つのクラスが他のクラスに依存することを許可するルールを定義しているため、コードのもつれが過剰になりません。 一般に、これはテンプレートのファミリであり、「UMLとデザインパターンのアプリケーション」という本のCraig Larmanのバージョンに焦点を当てています。 ここに図があります



基本的な考え方は、下位レベルのクラスは上位レベルのクラスに依存できないということです。 クラスをLayers



のレベルに配置すると、次の図のようになります。



レベルの境界を横切るすべての矢印は厳密に下に向けられていることに注意してください! TicketActivity



は、 GetNewTicket



下矢印を作成します。 GetNewTicket



は、下矢印のTicket



作成します。 匿名ticketListener



は、 PropertyChangeListener



インターフェース(下矢印)を実装します。 Ticket



リスナーにPropertyChangeListener



下矢印を通知します。 等



つまり、依存関係(継承、クラスのメンバーの型としての使用、パラメーター型または戻り値の型としての使用、型としてのローカル変数の使用)は、同じレベルまたはそれより低いレベルのクラスにのみ許可されます。



理論の別のドロップ、およびコードに移動します。



レベル割り当て



Domains



レベルのオブジェクトは、アプリケーションが動作するビジネスエンティティを反映します。 これらは、アプリケーションの配置方法とは無関係でなければなりません。 たとえば、 Ticket



positionInQueue



フィールドが存在するのは、ビジネス要件によるものです(アプリケーションの作成方法ではありません)。



Application



レベルは、アプリケーションロジックを配置できる場所の境界です(外観の整形を除く)。 有用な作業を行う必要がある場合、コードはここ(または以下)にあるはずです。



外観を使用して何かを行う必要がある場合、これはPresentation



レベルのクラスです。 したがって、このクラスには表示コードのみを含めることができ、ロジックは含めることができません。 ロジックについては、 Application



レベルからクラスにアクセスする必要があります。



特定のレベルのLayers



へのクラスの所属は条件付きです。 クラスは、要件を満たしている限り、特定のレベルにあります。 つまり、編集の結果として、クラスは別のレベルに移動したり、任意のレベルに適さなくなったりする可能性があります。



特定のクラスがどのレベルにあるべきかを判断する方法は? 控えめな発見的方法を共有しますが、一般的には、アクセシブルな理論を勉強することをお勧めします。 ここからでも始めてください



発見的

  1. アプリケーションがプレゼンテーションレベルを削除すると、すべての機能を実行できるはずです(結果のデモを除く)。 プレゼンテーションレベルのないアプリケーションには、チケットをリクエストするためのコード、チケット自体、およびそれにアクセスするためのコードが含まれています。
  2. あるクラスのオブジェクトが何かを表示するか、ユーザーのアクションに応答する場合、その場所はビューレベルにあります。
  3. 競合する場合は、クラスをいくつかに分割します。


コード



リポジトリhttps://github.com/SamSoldatenko/habr3には、 android.databinding



およびroboguice



を使用して構築された、ここで説明するアプリケーションが含まれています。 コードを見て、ここで私がどのような選択をし、どのような理由で行ったのかを簡単に説明します。

簡単な説明
  1. com.android.support:appcompat-v7



    依存関係が追加されたのは、商用開発がこのライブラリに依存して古いバージョンのAndroidをサポートするためです。

  2. @UiThread



    アノテーションを使用するために、 @UiThread



    com.android.support:support-annotations



    依存関係が追加されました(他にも多くの便利なアノテーションがあります)。

  3. 依存関係org.roboguice:roboguice



    依存関係注入のためのライブラリ。 Injectアノテーションを使用してパーツからアプリケーションを作成するために使用されます。 また、このライブラリを使用すると、リソース、ウィジェットリンクを埋め込むことができ、JSR-299のCDIイベントに似たメッセージ転送メカニズムが含まれます。

    • @Inject



      アノテーションを使用するTicketActivity



      は、 TicketSubsystem



      へのリンクをTicketSubsystem



      ます。
    • @InjectResource



      非同期タスクは、 @InjectResource



      アノテーションを使用して、チケットをロードする必要があるリソースからファイル名をReadTicketFromFile



      ます。
    • @Inject



      を使用するReadTicketFromFile



      は、 ReadTicketFromFile



      作成に使用するProvider



      を取得しProvider



    • その他


  4. org.roboguice:roboblender



    は、コンパイル時にorg.roboguice:roboguice



    すべての注釈のデータベースを作成し、実行時に使用します。

  5. roboguice



    ライブラリからの警告を抑制する設定をapp/lint.xml



    を追加しました。

  6. app/build.gradle



    dataBinding



    オプションは、 Expression Language



    EL



    )に類似したレイアウトファイルの特別な構文を有効にし、 Ticket



    TicketSubsystem



    をアクティブモデルにするために使用されるandroid.databinding



    パッケージを含みます。 その結果、ビューコードは大幅に簡素化され、レイアウトファイルの宣言に置き換えられます。 例:



     <TextView ... android:text="@{ts.ticket.number}" />
          
          





  7. .idea



    フォルダー.idea



    .gitignore



    に含まれて.gitignore



    Android Studio



    またはIDEA



    任意のバージョンを使用できます。 プロジェクトはbuild.gradle



    ファイルを介して完全にインポートおよび同期されます。

  8. gradlew



    ラッパーの構成は変更されません( gradlew



    gradlew.bat



    ファイル、およびgradlew.bat



    フォルダー)。 これは非常に効果的で便利なメカニズムです。

  9. app/build.gradle



    unitTests.returnDefaultValues = true



    を設定します。 これは、単体テストでのランダムエラーに対する保護と単体テストの簡潔さの妥協点です。 ここでは、ユニットテストの簡潔さを優先しました。

  10. org.mockito:mockito-core



    ライブラリは、単体テストでスタブを作成するために使用されます。 さらに、このライブラリを使用すると、 @Mock



    および@InjectMocks



    を使用して「テスト中のシステム」を記述することができます。 依存性注入を使用する場合、コンポーネントは依存性が使用される前に実装されることを「期待」します。 テストの前に、すべての依存関係も実装する必要があります。 Mockito



    は、テストされたクラスでスタブを作成および実装できます。
    これにより、特に埋め込みフィールドの表示が制限されている場合、テストコードが大幅に簡素化されます。 GetNewTicketTestを参照してください。

  11. Mockito



    なくRobolectric



    理由

    1. Android開発者は、この方法でローカルユニットテストを作成することをお勧めします。
    2. これにより、「編集」サイクルの最速パス-「テスト実行」-「結果」(TDDにとって重要)になります。
    3. Robolectricは、単体テストよりも統合テストに適しています。


  12. org.powermock:powermock-module-junit



    ライブラリorg.powermock:powermock-module-junit



    およびorg.powermock:powermock-api-mockito



    一部のものはプラグに置き換えることができません。 たとえば、静的メソッドを置き換えるか、基本クラスメソッドの呼び出しを抑制します。 これらの目的のために、 PowerMock



    はクラスローダーを置き換え、バイトコードを修正します。 TicketActivityTest



    では、 PowerMock



    RoboActionBarActivity.onCreate(Bundle)



    への呼び出しを抑制し、静的メソッドDataBindingUtil.setContentView



    への呼び出しからの戻り値を設定します

  13. 多くのクラスフィールドにパッケージローカルスコープがあるのはなぜですか?

    1. これはアプリケーションコードであり、ライブラリではありません。 つまり、クラスを使用するすべてのコードを制御します。 したがって、フィールドを非表示にする必要はありません。
    2. テストからのフィールドの可視性により、単体テストの記述が容易になります。


  14. なぜすべてのフィールドが公開されていないのですか?

    クラスのパブリックメンバーは、存在するクラスおよび将来表示される他のすべてのクラスに対して、クラスによって行われるコミットメントです。 また、ローカルパッケージは、同じパッケージに同時に入っている人のみを対象としています。 したがって、パッケージ内のすべてのクラスを更新する場合、パッケージローカルフィールドを変更(名前の変更、削除、新しいフィールドの追加)できます。

  15. LogInterface log



    変数が静的ではないのはなぜですか?

    1. 初期化コードを自分で記述する必要はありません。 DIはこれを改善します。
    2. ロガーをスタブに置き換えやすくするため。 特定の場合のログ出力は「指定」され、テストでチェックされます。


  16. RoboGuiceの類似クラスの子孫であるLogInterface



    LogImpl



    が必要なのはなぜですか?

    @ImplementedByアノテーション@ImplementedBy(LogImpl.class)



    使用してRoboguice構成を規定するには

  17. Ticket



    TicketSubsystem



    TicketSubsystem



    @UiThread



    アノテーションが必要な理由

    これらのクラスは、表示を更新するためにUIコンポーネントで使用されるonPropertyChanged



    イベントのソースです。 UIスレッドで呼び出しが行われることを保証する必要があります。

  18. TicketSubsystem



    コンストラクターで何が起こりますか?

    アプリケーションを起動した後、ファイルからデータをロードする必要があります。 Androidアプリケーションでは、これはApplication.onCreateイベントです。 しかし、この例では、そのようなクラスは追加されていません。 したがって、ファイルを読み取る必要がある瞬間は、 TicketSubsystem



    が作成される時間によって決まります( @Singleton



    アノテーションでマークされているTicketSubsystem



    、1つのコピーのみが作成されます)。 ただし、 TicketSubsystem



    コンストラクターでReadTicketFromFile



    を作成することはできません。まだ作成されていないReadTicketFromFile



    への参照が必要なためです。 したがって、 ReadTicketFromFile



    の作成は、ストリームの次のUIループまで延期されます。

  19. 再起動後のアプリケーションの動作を確認するには:

    1. [チケットを取得]をクリックします
    2. 表示されるのを待たずに、「ホーム」をクリックします
    3. コンソールで、 adb shell am kill ru.soldatenko.habr3



      実行しadb shell am kill ru.soldatenko.habr3



    4. アプリケーションを実行する






ありがとう



All Articles