onRestoreInstanceState
せずに
onRestoreInstanceState
から
onSaveInstanceState
に転送するための奇妙なソリューションが示されました。
android.os.Parcel
クラスの
writeStrongBinder(IBInder)
メソッドを使用します。
このようなソリューションは、Androidがアプリケーションをアンロードするまで正しく機能します。 そして、彼にはそれを行う権利があります。
...システムは、他のフォアグラウンドまたは可視プロセスのメモリを再利用するために、そのプロセスを安全に終了する場合があります...
( http://developer.android.com/intl/en/reference/android/app/Activity.html )
ただし、これはポイントではありません。 (このような再起動後にアプリケーションがその状態を復元する必要がない場合、このソリューションも適しています)。
しかし、そのような「シリアライズ不可能な」オブジェクトが使用される目的は、私には奇妙に思えました。 そこで、アプリケーションの表示状態を更新するために、非同期操作から
Activity
に呼び出しを転送します。
私はいつも、Smalltalk以来、どんな開発者もこの典型的な設計作業を認識すると思っていました。 しかし、私は間違っていたようです。
挑戦する
- ユーザーからのコマンド(
onClick()
)で、非同期操作を開始します - 操作が完了したら、
Activity
結果を表示します
特徴
- 操作の完了時に表示される
Activity
は
- チームが来たのと同じ
- 同じクラスの別のインスタンス(画面の回転)
- 別のクラスのインスタンス(ユーザーがアプリケーションで別の画面に切り替えた)
- 操作が完了すると、アプリケーションの単一の
Activty
が表示されないことが判明する場合があります
後者の場合、次回
Activity
開いたときに結果が表示されます。
解決策
MVC(アクティブモデル付き)およびレイヤー。
詳細なソリューション
記事の残りの部分では、MVCとレイヤーについて説明します。
具体例を挙げて説明します。 アプリケーション「電子キューへの電子チケット」を作成する必要があります。
- ユーザーは銀行支店に入り、アプリケーションの「チケットを取得」ボタンをクリックします。 アプリケーションはサーバーにリクエストを送信し、チケットを受信します。
- キューが開くと、アプリケーションは連絡する必要があるウィンドウの番号を表示します。
非同期操作を使用してサーバーからチケットを受け取ります。 また、非同期操作はファイルからチケットを読み取り(再起動後)、ファイルを削除します。
このようなアプリケーションは、単純なコンポーネントから構築できます。 例:
- チケットが配置されるコンポーネント(
TicketSubsystem
) - チケットが表示される
TicketActivity
および「チケットをTicketActivity
」ボタン - チケットのクラス(チケット番号、広告申込情報、ウィンドウ番号)
- 非同期チケット取得のクラス
最も興味深いのは、これらのコンポーネントがどのように相互作用するかです。
アプリケーションには、
TicketSubsystem
コンポーネントを含める必要はありません。 チケットは
静的フィールド
Ticket.currentTicket
、または
Ticket.currentTicket
クラス
android.app.Application
フィールド。
ただし、その条件は、ロールを実行できるオブジェクトからのチケットである/ないことであることが非常に重要です
MVC
-つまり、チケットが表示された(または置き換えられた)ときに通知を生成します。
TicketSubsystem
を
MVC
観点からモデルにすると、
Activity
はイベントをサブスクライブし、ロード時にチケット表示を更新できます。 この場合、
Activity
は
MVC
観点から
View
として機能し
View
。
その後、非同期操作「新しいチケットを取得」は、受信したチケットを
TicketSubsystem
記録するだけで、他のことを心配する必要はありません。
モデル
明らかに、チケットはモデルでなければなりません。 ただし、アプリケーションでは、チケットを空中に「吊るす」ことはできません。 さらに、チケットは最初は存在せず、非同期操作の完了時にのみ表示されます。 このことから、チケットが配置されるアプリケーションには何か他のものがなければならないことがわかります。 それを
TicketSubsystem
ます。 チケット自体も何らかの方法で提示する必要があり、それを
Ticket
クラスにします。 これらのクラスは両方とも、アクティブなモデルの役割を果たすことができる必要があります。
アクティブなモデルを構築する方法
アクティブなモデル-モデルは、変更が発生したというアイデアを通知します。 ウィキペディア
アクティブなモデルを作成するためのjavaのヘルパークラスがいくつかあります。 以下に例を示します。
-
java.beans
パッケージのPropertyChangeSupport
およびPropertyChangeListener
-
java.util
パッケージのObservable
およびObserver
-
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
次が必要です。
-
ticket
データを変更するときにUIウィジェットを更新する - リスナーが表示されたらチケットに接続します
- リスナーを
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
へのクラスの所属は条件付きです。 クラスは、要件を満たしている限り、特定のレベルにあります。 つまり、編集の結果として、クラスは別のレベルに移動したり、任意のレベルに適さなくなったりする可能性があります。
特定のクラスがどのレベルにあるべきかを判断する方法は? 控えめな発見的方法を共有しますが、一般的には、アクセシブルな理論を勉強することをお勧めします。 ここからでも始めてください 。
発見的
- アプリケーションがプレゼンテーションレベルを削除すると、すべての機能を実行できるはずです(結果のデモを除く)。 プレゼンテーションレベルのないアプリケーションには、チケットをリクエストするためのコード、チケット自体、およびそれにアクセスするためのコードが含まれています。
- あるクラスのオブジェクトが何かを表示するか、ユーザーのアクションに応答する場合、その場所はビューレベルにあります。
- 競合する場合は、クラスをいくつかに分割します。
コード
リポジトリhttps://github.com/SamSoldatenko/habr3には、
android.databinding
および
roboguice
を使用して構築された、ここで説明するアプリケーションが含まれています。 コードを見て、ここで私がどのような選択をし、どのような理由で行ったのかを簡単に説明します。
簡単な説明
-
com.android.support:appcompat-v7
依存関係が追加されたのは、商用開発がこのライブラリに依存して古いバージョンのAndroidをサポートするためです。
-
@UiThread
アノテーションを使用するために、@UiThread
:com.android.support:support-annotations
依存関係が追加されました(他にも多くの便利なアノテーションがあります)。
- 依存関係
org.roboguice:roboguice
依存関係注入のためのライブラリ。 Injectアノテーションを使用してパーツからアプリケーションを作成するために使用されます。 また、このライブラリを使用すると、リソース、ウィジェットリンクを埋め込むことができ、JSR-299のCDIイベントに似たメッセージ転送メカニズムが含まれます。
-
@Inject
アノテーションを使用するTicketActivity
は、TicketSubsystem
へのリンクをTicketSubsystem
ます。 -
@InjectResource
非同期タスクは、@InjectResource
アノテーションを使用して、チケットをロードする必要があるリソースからファイル名をReadTicketFromFile
ます。 -
@Inject
を使用するReadTicketFromFile
は、ReadTicketFromFile
作成に使用するProvider
を取得しProvider
。 - その他
-
-
org.roboguice:roboblender
は、コンパイル時にorg.roboguice:roboguice
すべての注釈のデータベースを作成し、実行時に使用します。
-
roboguice
ライブラリからの警告を抑制する設定をapp/lint.xml
を追加しました。
-
app/build.gradle
のdataBinding
オプションは、Expression Language
(EL
)に類似したレイアウトファイルの特別な構文を有効にし、Ticket
とTicketSubsystem
をアクティブモデルにするために使用されるandroid.databinding
パッケージを含みます。 その結果、ビューコードは大幅に簡素化され、レイアウトファイルの宣言に置き換えられます。 例:
<TextView ... android:text="@{ts.ticket.number}" />
-
.idea
フォルダー.idea
.gitignore
に含まれて.gitignore
、Android Studio
またはIDEA
任意のバージョンを使用できます。 プロジェクトはbuild.gradle
ファイルを介して完全にインポートおよび同期されます。
-
gradlew
ラッパーの構成は変更されません(gradlew
、gradlew.bat
ファイル、およびgradlew.bat
フォルダー)。 これは非常に効果的で便利なメカニズムです。
-
app/build.gradle
でunitTests.returnDefaultValues = true
を設定します。 これは、単体テストでのランダムエラーに対する保護と単体テストの簡潔さの妥協点です。 ここでは、ユニットテストの簡潔さを優先しました。
-
org.mockito:mockito-core
ライブラリは、単体テストでスタブを作成するために使用されます。 さらに、このライブラリを使用すると、@Mock
および@InjectMocks
を使用して「テスト中のシステム」を記述することができます。 依存性注入を使用する場合、コンポーネントは依存性が使用される前に実装されることを「期待」します。 テストの前に、すべての依存関係も実装する必要があります。Mockito
は、テストされたクラスでスタブを作成および実装できます。 これにより、特に埋め込みフィールドの表示が制限されている場合、テストコードが大幅に簡素化されます。 GetNewTicketTestを参照してください。
-
Mockito
なくRobolectric
理由
- Android開発者は、この方法でローカルユニットテストを作成することをお勧めします。
- これにより、「編集」サイクルの最速パス-「テスト実行」-「結果」(TDDにとって重要)になります。
- Robolectricは、単体テストよりも統合テストに適しています。
-
org.powermock:powermock-module-junit
ライブラリorg.powermock:powermock-module-junit
およびorg.powermock:powermock-api-mockito
一部のものはプラグに置き換えることができません。 たとえば、静的メソッドを置き換えるか、基本クラスメソッドの呼び出しを抑制します。 これらの目的のために、PowerMock
はクラスローダーを置き換え、バイトコードを修正します。TicketActivityTest
では、PowerMock
はRoboActionBarActivity.onCreate(Bundle)
への呼び出しを抑制し、静的メソッドDataBindingUtil.setContentView
への呼び出しからの戻り値を設定します
- 多くのクラスフィールドにパッケージローカルスコープがあるのはなぜですか?
- これはアプリケーションコードであり、ライブラリではありません。 つまり、クラスを使用するすべてのコードを制御します。 したがって、フィールドを非表示にする必要はありません。
- テストからのフィールドの可視性により、単体テストの記述が容易になります。
- なぜすべてのフィールドが公開されていないのですか?
クラスのパブリックメンバーは、存在するクラスおよび将来表示される他のすべてのクラスに対して、クラスによって行われるコミットメントです。 また、ローカルパッケージは、同じパッケージに同時に入っている人のみを対象としています。 したがって、パッケージ内のすべてのクラスを更新する場合、パッケージローカルフィールドを変更(名前の変更、削除、新しいフィールドの追加)できます。
-
LogInterface log
変数が静的ではないのはなぜですか?
- 初期化コードを自分で記述する必要はありません。 DIはこれを改善します。
- ロガーをスタブに置き換えやすくするため。 特定の場合のログ出力は「指定」され、テストでチェックされます。
- RoboGuiceの類似クラスの子孫である
LogInterface
とLogImpl
が必要なのはなぜですか?
@ImplementedByアノテーション@ImplementedBy(LogImpl.class)
使用してRoboguice構成を規定するには
-
Ticket
TicketSubsystem
とTicketSubsystem
の@UiThread
アノテーションが必要な理由
これらのクラスは、表示を更新するためにUIコンポーネントで使用されるonPropertyChanged
イベントのソースです。 UIスレッドで呼び出しが行われることを保証する必要があります。
-
TicketSubsystem
コンストラクターで何が起こりますか?
アプリケーションを起動した後、ファイルからデータをロードする必要があります。 Androidアプリケーションでは、これはApplication.onCreateイベントです。 しかし、この例では、そのようなクラスは追加されていません。 したがって、ファイルを読み取る必要がある瞬間は、TicketSubsystem
が作成される時間によって決まります(@Singleton
アノテーションでマークされているTicketSubsystem
、1つのコピーのみが作成されます)。 ただし、TicketSubsystem
コンストラクターでReadTicketFromFile
を作成することはできません。まだ作成されていないReadTicketFromFile
への参照が必要なためです。 したがって、ReadTicketFromFile
の作成は、ストリームの次のUIループまで延期されます。
- 再起動後のアプリケーションの動作を確認するには:
- [チケットを取得]をクリックします
- 表示されるのを待たずに、「ホーム」をクリックします
- コンソールで、
adb shell am kill ru.soldatenko.habr3
実行しadb shell am kill ru.soldatenko.habr3
- アプリケーションを実行する
ありがとう