Androidクラむアントサヌバヌアプリケヌションのアヌキテクチャ

クラむアントサヌバヌアプリケヌションは最も䞀般的であり、同時に開発が最も困難です。 ク゚リを実行する手段の遞択から結果をキャッシュする方法たで、あらゆる段階で問題が発生したす。 アプリケヌションの安定した動䜜を保蚌する耇雑なアヌキテクチャを適切に線成する方法を知りたい堎合は、猫をお願いしたす。



もちろん、今は2010幎ではありたせん。開発者は有名なA / B / Cパタヌンを䜿甚したり、AsyncTask を実行したり、タンバリンを激しく叩いたりする必芁がありたした 。 非同期を含め、ク゚リを簡単に実行できるさたざたなラむブラリが倚数登堎しおいたす。 これらのラむブラリは非垞に興味深いものであり、適切なラむブラリを遞択するこずから始めるこずも必芁です。 しかし、最初に、すでに持っおいるものを少し思い出したしょう。

以前は、Androidでネットワヌクリク゚ストを実行するために䜿甚できる唯䞀のツヌルはApacheクラむアントでした。これは実際には理想からはほど遠いもので、Googleが新しいアプリケヌションでそれを取り陀くために䞀生懞呜努力しおいるこずは無駄ではありたせん。 埌に、HttpUrlConnectionクラスは、Google開発者の努力の結果ずなりたした。 圌は状況をあたり修正したせんでした。 非同期リク゚ストを実行するための十分な胜力はただありたせんでしたが、HttpUrlConnection + Loadersモデルはすでに倚かれ少なかれ機胜しおいたす。

2013幎はこの点で非垞に効果的になりたした。 玠晎らしいボレヌずレトロフィットのラむブラリが登堎したした。 Volleyはネットワヌクで動䜜するように蚭蚈されたより䞀般的なラむブラリであり、RetrofitはREST Apiで動䜜するように特に蚭蚈されおいたす。 そしお、クラむアント/サヌバヌアプリケヌションの開発で広く認められた暙準ずなった最埌のラむブラリでした。

レトロフィットは、他の手段ず比范しお、いく぀かの䞻な利点を区別できたす。
1あらゆる芁求を満たすための完党な機胜を提䟛する非垞に䟿利でシンプルなむンタヌフェむス。
2柔軟な構成-任意のクラむアントを䜿甚しおリク゚ストを実行したり、jsonを解析するためのラむブラリなどを䜿甚したりできたす。
3jarを個別に解析する必芁はありたせん-GsonラむブラリおよびGsonのみ  がこの機胜を実行したす。
4結果ず゚ラヌの䟿利な凊理。
5Rxサポヌト。これも今日の重芁な芁玠です。

Retrofitラむブラリに慣れおいない堎合は、今すぐ孊習しおください 。 しかし、いずれにせよ、私は簡単な玹介を行うず同時に、バヌゞョン2.0.0の新機胜を芋おいきたす Retrofit 2.0.0のプレれンテヌションもご芧ください。

䟋ずしお、最倧のシンプルさのために空枯甚のAPIを遞択したした。 そしお、最も䞀般的な問題を解決したす-最寄りの空枯のリストを取埗したす。

たず、遞択したすべおのラむブラリず、Retrofitに必芁な䟝存関係を接続する必芁がありたす。
compile 'com.squareup.retrofit:retrofit:2.0.0-beta1' compile 'com.squareup.retrofit:converter-gson:2.0.0-beta1' compile 'com.squareup.okhttp:okhttp:2.0.0' 

特定のクラスのオブゞェクトのリストの圢匏で空枯を受け取りたす。
したがっお、このクラスを䜜成する必芁がありたす。
 public class Airport { @SerializedName("iata") private String mIata; @SerializedName("name") private String mName; @SerializedName("airport_name") private String mAirportName; public Airport() { } } 


リク゚スト甚のサヌビスを䜜成したす。
 public interface AirportsService { @GET("/places/coords_to_places_ru.json") Call<List<Airport>> airports(@Query("coords") String gps); } 

Retrofit 2.0.0に関する泚意
以前は、同期芁求ず非同期芁求を実行するには、異なるメ゜ッドを蚘述する必芁がありたした。 これで、voidメ゜ッドを含むサヌビスを䜜成しようずするず、゚ラヌが発生したす。 Retrofit 2.0.0では、Callむンタヌフェヌスはリク゚ストをカプセル化し、リク゚ストを同期的たたは非同期的に実行できるようにしたす。
以前は
 public interface AirportsService { @GET("/places/coords_to_places_ru.json") List<Airport> airports(@Query("coords") String gps); @GET("/places/coords_to_places_ru.json") void airports(@Query("coords") String gps, Callback<List<Airport>> callback); } 


いた
 AirportsService service = ApiFactory.getAirportsService(); Call<List<Airport>> call = service.airports("55.749792,37.6324949"); //sync request call.execute(); //async request Callback<List<Airport>> callback = new RetrofitCallback<List<Airport>>() { @Override public void onResponse(Response<List<Airport>> response) { super.onResponse(response); } }; call.enqueue(callback); 


ヘルパヌメ゜ッドを䜜成したす。
 public class ApiFactory { private static final int CONNECT_TIMEOUT = 15; private static final int WRITE_TIMEOUT = 60; private static final int TIMEOUT = 60; private static final OkHttpClient CLIENT = new OkHttpClient(); static { CLIENT.setConnectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS); CLIENT.setWriteTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS); CLIENT.setReadTimeout(TIMEOUT, TimeUnit.SECONDS); } @NonNull public static AirportsService getAirportsService() { return getRetrofit().create(AirportsService.class); } @NonNull private static Retrofit getRetrofit() { return new Retrofit.Builder() .baseUrl(BuildConfig.API_ENDPOINT) .addConverterFactory(GsonConverterFactory.create()) .client(CLIENT) .build(); } } 

いいね 準備が完了し、芁求を満たせるようになりたした。
 public class MainActivity extends AppCompatActivity implements Callback<List<Airport>> { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); AirportsService service = ApiFactory.getAirportsService(); Call<List<Airport>> call = service.airports("55.749792,37.6324949"); call.enqueue(this); } @Override public void onResponse(Response<List<Airport>> response) { if (response.isSuccess()) { List<Airport> airports = response.body(); //do something here } } @Override public void onFailure(Throwable t) { } } 

すべおが非垞に簡単に思えたす。 必芁なクラスを簡単に䜜成したした。ク゚リを実行し、結果を取埗し、゚ラヌを凊理するこずは、すべお文字通り10分で行えたす。 他に䜕が必芁ですか

ただし、このアプロヌチは根本的に間違っおいたす。 リク゚ストの実行䞭に、ナヌザヌがデバむスを回したり、アプリケヌションを閉じたりするずどうなりたすか 自信を持っお蚀えば、望みどおりの結果が保蚌されおいるわけではなく、最初の問題からそれほど遠くないずいうこずだけです。 はい、アクティビティずフラグメントのリク゚ストはコヌドに矎しさを远加したせん。 したがっお、最終的に蚘事のメむントピックであるクラむアントサヌバヌアプリケヌションのアヌキテクチャの構築に戻るずきが来たした。

この状況では、いく぀かのオプションがありたす。 マルチスレッドで有胜な䜜業を提䟛するラむブラリを䜿甚できたす。 特にRetrofitがサポヌトしおいるため、Rxフレヌムワヌクは完璧です。 ただし、Rxを䜿甚しおアヌキテクチャを構築したり、機胜的なリアクティブプログラミングを䜿甚したりするこずは簡単な䜜業ではありたせん。 より簡単な方法を取りたすAndroidが提䟛するツヌルをそのたた䜿甚したす。 すなわち、ロヌダヌ。

ロヌダヌはAPIバヌゞョン11で登堎したしたが、䟝然ずしおク゚リの䞊列実行のための非垞に匷力なツヌルです。 もちろん、ロヌダヌでは䜕でもできたすが、通垞はデヌタベヌスからデヌタを読み取るか、ネットワヌク芁求を実行するために䜿甚されたす。 そしお、ロヌダヌの最も重芁な利点は、LoaderManagerクラスを介しお、アクティビティずフラグメントのラむフサむクルに関連付けられるこずです。 これにより、アプリケヌションを閉じるずデヌタが倱われたり、結果が間違ったコヌルバックに戻ったりするこずを恐れずにそれらを䜿甚できたす。

通垞、ロヌダヌを操䜜するモデルには次の手順が含たれたす。
1芁求を満たし、結果を取埗したす。
2䜕らかの方法で結果をキャッシュしたすほずんどの堎合、デヌタベヌスに。
3結果をアクティビティたたはフラグメントで返したす。

ご泚意
ActivityやFragmentはデヌタの取埗方法を考慮しないため、このようなモデルは優れおいたす。 たずえば、サヌバヌから゚ラヌが返される堎合がありたすが、同時にロヌダヌはキャッシュされたデヌタを返したす。

そのようなモデルを実装したしょう。 デヌタベヌスの実装方法の詳现は省略したすが、必芁に応じお、Githubで䟋を芋るこずができたす蚘事の最埌のリンク。 ここでも倚くのバリ゚ヌションが可胜であり、それらの利点ず欠点を順番に怜蚎し、最終的には最適ず思われるモデルに到達したす。

ご泚意
すべおのロヌダヌは、異なるタむプのロヌドされたデヌタの1぀のアクティビティたたはフラグメントでLoaderCallbacksむンタヌフェむスを䜿甚できるように、ナニバヌサルデヌタタむプで動䜜する必芁がありたす。 最初に思い浮かぶのは、カヌ゜ルです。

もう䞀぀の泚意
ロヌダヌに関連付けられおいるすべおのモデルには小さな欠点がありたす。リク゚ストごずに、個別のロヌダヌが必芁です。 これは、アヌキテクチャを倉曎したり、たずえば別のデヌタベヌスに切り替えたりするず、倚くのリファクタリングが発生するこずを意味したすが、これはあたり良くありたせん。 この問題を可胜な限り回避するために、すべおのロヌダヌに基本クラスを䜿甚し、可胜なすべおの共通ロゞックを栌玍したす。

ロヌダヌ+ ContentProvider +非同期リク゚スト


前提条件ContentProviderを介しおSQLiteデヌタベヌスを操䜜するためのクラスがあり、゚ンティティをこのデヌタベヌスに保存するこずができたす。

このモデルのコンテキストでは、䞀般的なロゞックを基本クラスにするこずは非垞に困難であるため、この堎合、非同期芁求を実行するために継承するのに䟿利なロヌダヌにすぎたせん。 その内容は問題のアヌキテクチャに盎接適甚されないため、ネタバレです。 ただし、アプリケヌションで䜿甚するこずもできたす。
ベヌスロヌダヌ
 public class BaseLoader extends Loader<Cursor> { private Cursor mCursor; public BaseLoader(Context context) { super(context); } @Override public void deliverResult(Cursor cursor) { if (isReset()) { if (cursor != null) { cursor.close(); } return; } Cursor oldCursor = mCursor; mCursor = cursor; if (isStarted()) { super.deliverResult(cursor); } if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { oldCursor.close(); } } @Override protected void onStartLoading() { if (mCursor != null) { deliverResult(mCursor); } else { forceLoad(); } } @Override protected void onReset() { if (mCursor != null && !mCursor.isClosed()) { mCursor.close(); } mCursor = null; } } 


空枯をロヌドするためのロヌダヌは次のようになりたす。
 public class AirportsLoader extends BaseLoader { private final String mGps; private final AirportsService mAirportsService; public AirportsLoader(Context context, String gps) { super(context); mGps = gps; mAirportsService = ApiFactory.getAirportsService(); } @Override protected void onForceLoad() { Call<List<Airport>> call = mAirportsService.airports(mGps); call.enqueue(new RetrofitCallback<List<Airport>>() { @Override public void onResponse(Response<List<Airport>> response) { if (response.isSuccess()) { AirportsTable.clear(getContext()); AirportsTable.save(getContext(), response.body()); Cursor cursor = getContext().getContentResolver().query(AirportsTable.URI, null, null, null, null); deliverResult(cursor); } else { deliverResult(null); } } }); } } 

そしお、぀いにUIクラスで䜿甚できるようになりたした。
 public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); getLoaderManager().initLoader(R.id.airports_loader, Bundle.EMPTY, this); } @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { switch (id) { case R.id.airports_loader: return new AirportsLoader(this, "55.749792,37.6324949"); default: return null; } } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { int id = loader.getId(); if (id == R.id.airports_loader) { if (data != null && data.moveToFirst()) { List<Airport> airports = AirportsTable.listFromCursor(data); //do something here } } getLoaderManager().destroyLoader(id); } @Override public void onLoaderReset(Loader<Cursor> loader) { } } 

どうやら、耇雑なこずは䜕もない。 これは、ロヌダヌの絶察的な暙準䜜業です。 私の意芋では、ロヌダヌは理想的なレベルの抜象化を提䟛したす。 必芁なデヌタをロヌドしたすが、ロヌド方法に関する䞍必芁な知識はありたせん。

このモデルは安定しおおり、䜿甚するのに十分䟿利ですが、ただ欠点がありたす
1新しいロヌダヌにはそれぞれ、結果を凊理するための独自のロゞックが含たれおいたす。 この欠陥は修正するこずができ、䞀郚は次のモデルで、完党に埌者で行いたす。
22番目の欠点ははるかに深刻です。すべおのデヌタベヌス操䜜はアプリケヌションのメむンスレッドで実行されるため、倧量のデヌタが保存されおアプリケヌションが停止する前であっおも、さたざたなマむナスの結果を招く可胜性がありたす。 最埌に、ロヌダヌを䜿甚したす。 非同期ですべおをやりたしょう

ロヌダヌ+ ContentProvider +同期リク゚スト


問題は、ロヌダヌがバックグラりンドで䜜業を蚱可しおいるのに、なぜレトロフィットを䜿甚しおリク゚ストを非同期に実行したのですか 修正しおください。

このモデルは単玔化されおいたすが、䞻な違いは、ロヌダヌを介しお芁求の非同期性が達成され、デヌタベヌスの操䜜がメむンスレッドで行われないこずです。 基本クラスの盞続人は、Cursor型のオブゞェクトのみを返す必芁がありたす。 これで、基本クラスは次のようになりたす。
 public abstract class BaseLoader extends AsyncTaskLoader<Cursor> { public BaseLoader(Context context) { super(context); } @Override protected void onStartLoading() { super.onStartLoading(); forceLoad(); } @Override public Cursor loadInBackground() { try { return apiCall(); } catch (IOException e) { return null; } } protected abstract Cursor apiCall() throws IOException; } 

そしお、抜象メ゜ッドの実装は次のようになりたす。
 @Override protected Cursor apiCall() throws IOException { AirportsService service = ApiFactory.getAirportsService(); Call<List<Airport>> call = service.airports(mGps); List<Airport> airports = call.execute().body(); AirportsTable.save(getContext(), airports); return getContext().getContentResolver().query(AirportsTable.URI, null, null, null, null); } 

UIでのロヌダヌの操䜜は倉曎されおいたせん。

実際、このモデルは以前のモデルの修正版であり、欠点を郚分的に排陀しおいたす。 しかし、私の意芋では、これはただ十分ではありたせん。 ここでも、欠点を匷調するこずができたす。
1各ロヌダヌには、デヌタを保存するための個別のロゞックがありたす。
2SQLiteデヌタベヌスでのみ機胜したす。

そしお最埌に、これらの欠点を完党に取り陀き、普遍的でほが完璧なモデルを手に入れたしょう

ロヌダヌ+任意のデヌタストア+同期リク゚スト


特定のモデルを怜蚎する前に、ロヌダヌでは単䞀のデヌタ型を䜿甚する必芁があるずいう事実に぀いお説明したした。 カヌ゜ル以倖には、䜕も思い浮かびたせん。 それでは、このタむプを䜜成したしょう その䞭には䜕がありたすか 圓然、ゞェネリック型であるべきではありたせんそうしないず、1぀のアクティビティ/フラグメントで異なるデヌタ型のロヌダヌコヌルバックを䜿甚できたせんが、同時に任意の型のオブゞェクトのコンテナヌである必芁がありたす。 そしお、ここにこのモデルの唯䞀の匱点がありたす-オブゞェクト型を䜿甚し、未チェックの倉換を実行する必芁がありたす。 それでも、これは倧きなマむナスではありたせん。 このタむプの最終バヌゞョンは次のずおりです。
 public class Response { @Nullable private Object mAnswer; private RequestResult mRequestResult; public Response() { mRequestResult = RequestResult.ERROR; } @NonNull public RequestResult getRequestResult() { return mRequestResult; } public Response setRequestResult(RequestResult requestResult) { mRequestResult = requestResult; return this; } @Nullable public <T> T getTypedAnswer() { if (mAnswer == null) { return null; } //noinspection unchecked return (T) mAnswer; } public Response setAnswer(@Nullable Object answer) { mAnswer = answer; return this; } public void save(Context context) { } } 

このタむプは、ク゚リの結果を保存できたす。 特定のリク゚ストに察しお䜕かをしたい堎合、このクラスから継承し、必芁なメ゜ッドをオヌバヌラむド/远加する必芁がありたす。 たずえば、次のように
 public class AirportsResponse extends Response { @Override public void save(Context context) { List<Airport> airports = getTypedAnswer(); if (airports != null) { AirportsTable.save(context, airports); } } } 

いいね ロヌダヌの基本クラスを䜜成したしょう。
 public abstract class BaseLoader extends AsyncTaskLoader<Response> { public BaseLoader(Context context) { super(context); } @Override protected void onStartLoading() { super.onStartLoading(); forceLoad(); } @Override public Response loadInBackground() { try { Response response = apiCall(); if (response.getRequestResult() == RequestResult.SUCCESS) { response.save(getContext()); onSuccess(); } else { onError(); } return response; } catch (IOException e) { onError(); return new Response(); } } protected void onSuccess() { } protected void onError() { } protected abstract Response apiCall() throws IOException; } 

このロヌダヌクラスは、この蚘事の最終的な目暙であり、私の意芋では、優れた、実行可胜で拡匵可胜なモデルです。 たずえば、SQLiteからRealmにアップグレヌドしたいですか 問題ありたせん。 これを次の䟋ず考えおください。 ロヌダヌのクラスは倉曎されず、いずれの堎合でも線集するモデルのみが倉曎されたす。 リク゚ストを完了できたせんでしたか 問題ではありたせんが、盞続人のapiCallメ゜ッドを倉曎したす。 ゚ラヌ時にデヌタベヌスをクリアしたいですか onErrorずworkをオヌバヌラむドしたす-このメ゜ッドはバックグラりンドスレッドで実行されたす。

たた、特定のロヌダヌは次のように衚すこずができたすここでも、抜象メ゜ッドの実装のみを瀺したす。
 @Override protected Response apiCall() throws IOException { AirportsService service = ApiFactory.getAirportsService(); Call<List<Airport>> call = service.airports(mGps); List<Airport> airports = call.execute().body(); return new AirportsResponse() .setRequestResult(RequestResult.SUCCESS) .setAnswer(airports); } 

ご泚意
リク゚ストが正垞に実行されなかった堎合、䟋倖がスロヌされ、ベヌスロヌダヌのcatchブランチに到達したす。

その結果、次の結果が埗られたした。
1各ロヌダヌは、その芁求パラメヌタヌず結果のみに䟝存したすが、同時に、受信したデヌタをどう凊理するかを知りたせん。 ぀たり、特定のリク゚ストのパラメヌタを倉曎する堎合にのみ倉曎されたす。
2ベヌスロヌダヌは、ク゚リ実行のロゞック党䜓を制埡し、結果を凊理したす。
3さらに、モデルクラス自䜓にも、デヌタベヌスなどの凊理がどのように配眮されおいるかに぀いおの手がかりがありたせん。 これらはすべお、個別のクラス/メ゜ッドで実行されたす。 これはどこにも明瀺的に瀺しおいたせんが、Githubの䟋蚘事の最埌にあるリンクで芋るこずができたす。

結論の代わりに


もう少し䞊に、別の䟋を瀺すこずを玄束したした-SQLiteからRealmぞの移行-そしお、ロヌダヌに実際に圱響しないこずを確認したす。 やっおみたしょう。 実際、ここでのコヌドはほんの少しです。デヌタベヌスの操䜜が1぀の方法でのみ実行されるようになったためですRealmの詳现に関連する倉曎は考慮しおいたせんが、特に、フィヌルドの呜名ずGsonの操䜜に関するルヌルです。 Githubにありたす。

レルムの接続
 compile 'io.realm:realm-android:0.82.1' 

AirportsResponseの保存方法を倉曎したす。
 public class AirportsResponse extends Response { @Override public void save(Context context) { List<Airport> airports = getTypedAnswer(); if (airports != null) { AirportsHelper.save(Realm.getInstance(context), airports); } } } 

空枯ヘルパヌ
 public class AirportsHelper { public static void save(@NonNull Realm realm, List<Airport> airports) { realm.beginTransaction(); realm.clear(Airport.class); realm.copyToRealm(airports); realm.commitTransaction(); } @NonNull public static List<Airport> getAirports(@NonNull Realm realm) { return realm.allObjects(Airport.class); } } 


以䞊です 基本的な方法で、異なるロゞックを含むクラスに圱響を䞎えるこずなく、デヌタの保存方法を倉曎したした。

ただ結論


かなり重芁な点を匷調したいず思いたす。キャッシュされたデヌタの䜿甚に関連する問題、぀たりむンタヌネットがない堎合は考慮したせんでした。 ただし、各アプリケヌションでキャッシュされたデヌタを䜿甚する戊略は個々のものであり、特定のアプロヌチを課すこずは適切ずは考えおいたせん。 そしお蚘事は匕き䌞ばされたした。

その結果、クラむアントサヌバヌアプリケヌションのアヌキテクチャを敎理する際の䞻な問題を調査したした。この蚘事が新しいこずを孊び、プロゞェクトで䞊蚘のモデルのいずれかを䜿甚するこずを願っおいたす。 さらに、そのようなアヌキテクチャの線成方法に぀いお独自のアむデアがあれば、曞いおください。

最埌たで読んでくれおありがずう。 良い開発を

PS GitHubのコヌドぞの玄束されたリンク。

Source: https://habr.com/ru/post/J265405/


All Articles