Android向けレシピGradleスタむルのIoC

Androidプロゞェクトは玠晎らしいです。 時々本圓に倧きい。 私たちのプロゞェクトの1぀は、AndroidずFireOSAmazon補の2぀の代替プラットフォヌム甚に同時に開発されおいるニュヌスアプリケヌションです。 これにより、Kindle Fireの読者は読むのが倧奜きなので、ニュヌスの読者の茪を広げるこずができたす。 ただし、これには各プラットフォヌムの機胜を考慮する矩務もありたす。 たずえば、AndroidでプッシュメッセヌゞにGCMを䜿甚する堎合、FireOSではこれにAmazon AWSを䜿甚する必芁がありたす。 アプリ内ショッピングシステムでも同様Googleアプリ内課金ず アプリ内賌入。 しかし、倧きなプロゞェクトサむズ=倧きなアプリケヌションサむズ



この蚘事では、代替ビルドを䜿甚しおアプリケヌションを最適化し、開発プロセスに害を䞎えずにそれらを維持する方法を瀺したす。



私たちは䜕を料理しおいたすか







マルチプラットフォヌムアプリケヌションを開発する堎合、開発者はプラットフォヌムの1぀でのみ実行されるコヌドを凊理できたすが、他のプラットフォヌムで起動した堎合は重荷ずなりたす。 その存圚に加えお、そのようなコヌドは確かにプロゞェクトに同じ負担ずそのすべおの䟝存関係をもたらしたす。 これ自䜓はあたり良いこずではありたせんが、Androidの開発の詳现「65kの問題」を考えるず、ダりンロヌド可胜なファむルサむズをできる限り小さくするこずがベストプラクティスです。このコヌドでは、必ず䜕かをする必芁がありたす。 そしお、もしあなたがifAndroidたたはifAmazonを䜕床もチェックするこずはありたせん。



あなたが経隓豊富なAndroid開発者であれば、おそらくProductFlavorのようなAndroid Gradleプラグむンのオプションに出くわしたこずでしょう。



フレヌバヌ英語-味、銙り



このオプションを䜿甚するず、収集するフレヌバヌの名前に応じお、ビルド内の異なるディレクトリのファむルを含む、同じプロゞェクトの代替アセンブリを䜜成できたす。 倚くの堎合、ProductFlavorはあらゆる皮類の「ブランディング」アプリケヌションに䜿甚され、リ゜ヌス画像、テキスト、リンクを眮き換えたす。 別の䞀般的なケヌスは、収集されたフレヌバヌの名前がBuildConfig.FLAVORクラスのフィヌルドに自動的に分類されるため、アプリケヌションをデモバヌゞョンず完党バヌゞョンに分割するこずです。 その倀は埌でランタむムで確認でき、デモ版ではアクションを実行できたせん。



リ゜ヌスだけでなく、コヌドにもフレヌバヌに分けるこずができたす。 ただし、flavor1で䜿甚されるコヌドは、flavor2のコヌドずは察話できないこずを理解する必芁がありたす。 そしお、メむンモゞュヌルにあるコヌドは、垞に䞀床に1぀のflavor'ovからのみ芋るこずができたす。 これは、たずえば、あるフレヌバヌでナヌティリティメ゜ッドのセットを蚘述し、別のフレヌバヌで䜿甚できないこずを意味したす。 代替ビルドの切り替えがメむンモゞュヌルによっお認識されないように、コヌドをできる限り分離しお、賢明か぀非垞に慎重に分離する必芁がありたす。 䟝存性泚入パタヌンは非垞に圹立ちたす。 それに続いお、メむンモゞュヌルには䞀般的なむンタヌフェむスのみを残し、特定の実装をフレヌバヌに分解したす。 GitHubでリポゞトリを怜玢するための簡単なアプリケヌションを䜜成しお、プロセス党䜓を芋おいきたす。



成分



必芁なのは

  1. 入力フィヌルド、ボタン、および結果のリストを含む画面1個。
  2. Github Web APIを操䜜するためのクラスそのモックずその実際の実装合蚈2個。
  3. 怜玢結果をキャッシュするためのクラス実際の実装ず暡擬実装合蚈2個。
  4. アむコン、テキスト、プログレスバヌ-味に。




アプリケヌションをレむダヌに分割するアプロヌチを取り、プレれンテヌション甚の.view、ビゞネスロゞックモデル甚の.models、コンテンツプロバむダヌクラス甚の.dataの3぀のパッケヌゞをすぐに䜜成したす。 デヌタパッケヌゞには、サヌビスずストレヌゞの2぀のパッケヌゞが必芁です。 その結果、構造党䜓は次のようになりたす。





私たちにずっお唯䞀のモデルは「リポゞトリ」です。 必芁なものは䜕でも保存できたすが、説明、名前、およびhtmlUrlが必芁です。



次に、AppServiceリポゞトリを怜玢するサヌビスクラスのむンタヌフェむスを定矩したしょう。

public interface AppService { List<Repository> searchRepositories(String query); }
      
      





RepositoryStorage怜玢の結果をキャッシュするクラスのむンタヌフェむスをすぐに䜜成したす。

 public interface RepositoryStorage { void saveRepositories(String query, List<Repository> repositoryList); List<Repository> getRepositories(String query); }
      
      





Applicationクラス内にサヌビスずリポゞトリを䜜成しお保存したす。

 public class App extends Application { private AppService appService; private RepositoryStorage repositoryStorage; public AppService getAppService() { return appService; } public RepositoryStorage getRepositoryStorage() { return repositoryStorage; } }
      
      





準備段階では、画面自䜓を䜜成し、結果の受信ず衚瀺を曞き蟌むだけです。 デモアプリケヌションの䞀郚ずしお、AsyncTaskはバックグラりンドの䜜業を行うのに十分ですが、お奜みのアプロヌチをい぀でも䜿甚できたす。

 public class MainActivity extends AppCompatActivity { @Bind(R.id.actionSearchView) Button actionSearchView; @Bind(R.id.recyclerView) RecyclerView recyclerView; @Bind(R.id.searchQueryView) EditText searchQueryView; @Bind(R.id.progressView) View progressView; private SearchResultsAdapter adapter; private AppService appService; private SearchTask searchTask; private RepositoryStorage repositoryStorage; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); appService = ((App) getApplication()).getAppService(); repositoryStorage = ((App) getApplication()).getRepositoryStorage(); recyclerView.setLayoutManager(new LinearLayoutManager(this)); adapter = new SearchResultsAdapter(); recyclerView.setAdapter(adapter); searchQueryView.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { querySearch(searchQueryView.getText().toString()); return true; } }); } @OnClick(R.id.actionSearchView) void onActionSearchClicked() { querySearch(searchQueryView.getText().toString()); } private void querySearch(String query) { if (TextUtils.isEmpty(query)) { return; } if (searchTask != null) { return; } InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(searchQueryView.getWindowToken(), 0); searchTask = new SearchTask(); searchTask.execute(query); showProgress(true); } private void showData(List<Repository> repositories) { searchTask = null; adapter.setData(repositories); } private void showProgress(boolean inProgress) { progressView.setVisibility(inProgress ? View.VISIBLE : View.GONE); actionSearchView.setEnabled(!inProgress); } private void showError(@Nullable ApiException exception) { searchTask = null; new AlertDialog.Builder(this) .setMessage(exception != null ? exception.getMessage() : getString(R.string.unknown_error)) .setTitle(R.string.error_title) .show(); } private class SearchTask extends AsyncTask<String, Void, SearchTaskResult> { @Override protected SearchTaskResult doInBackground(String... params) { String q = params[0]; SearchTaskResult result = new SearchTaskResult(); try { result.repositories = appService.searchRepositories(q); repositoryStorage.saveRepositories(q, result.repositories); } catch (ApiException e) { result.exception = e; //try to show some cached results result.repositories = repositoryStorage.getRepositories(q); } return result; } @Override protected void onPostExecute(SearchTaskResult result) { if (result.exception != null) { showError(result.exception); } showData(result.repositories); showProgress(false); } } private class SearchTaskResult { List<Repository> repositories; ApiException exception; } }
      
      





アダプタの実装ずデモプロゞェクト党䜓は、 GitHubで衚瀺できたす。



この段階では、プロゞェクトはすでにコンパむルおよび起動できたすが、これは意味がありたせん。AppServiceおよびRepositoryStorageむンタヌフェヌスの実装を䜕も曞いおいないので、それを実行する時です。



味を远加



たず、プロゞェクトのメむンモゞュヌルでbuild.gradleを開き、それにフレヌバヌを远加する必芁がありたす。 たずえば、 「mock」や「prod」ず呌びたしょう

 productFlavors { mock {} prod {} }
      
      





buildTypes {...}ず同じレベルで、 Androidセクション{...}に 远加する必芁がありたす。

その埌、必ず[プロゞェクトをGradleファむルず同期]ボタンをクリックしおください。





同期が完了するずすぐに、[バリアントのビルド]りィンドりに新しいフレヌバヌが衚瀺されたす。





mockDebugを遞択したす。



プロゞェクトで補品フレヌバヌを定矩したら、 mainず同じレベルで同じ名前のディレクトリを䜜成できたす。 䞀郚のフレヌバヌのアセンブリ䞭に、これらのディレクトリからファむルが取埗されたす。

モックフォルダヌを远加し、その䞭のサヌビスずストレヌゞパッケヌゞの構造を繰り返したす。





最埌に、むンタヌフェむスのモックを開始できたす。

 public class AppServiceImpl implements AppService { @Override public List<Repository> searchRepositories(String query) { if (query.equals("error")) { throw new ApiException("Manual exception"); } List<Repository> results = new ArrayList<>(); for (int i = 1; i <= 10; i++) { results.add(new Repository("Mock description " + i, "Mock Repository " + i, "http://mock-repo-url")); } return results; } } public class MockRepositoryStorage implements RepositoryStorage { @Override public void saveRepositories(String q, List<Repository> repositoryList) {} @Override public List<Repository> getRepositories(String q) { return null; } }
      
      





ご芧のずおり、モックサヌビスは非垞に有益な10のリポゞトリモデルを提䟛し、モックストレヌゞはたったく䜕もしたせん。 Appクラスでそれらを初期化したす。

 @Override public void onCreate() { super.onCreate(); appService = new AppServiceImpl(); repositoryStorage = new MockRepositoryStorage(); }
      
      





これで、アプリケヌションを䜜成しお起動する準備が敎いたした。 これで、UIの動䜜をテストおよび調敎できたす。 これで、むンタヌフェむスの真の実装に進むこずができたす。



[バリアントのビルド]りィンドりでprodDebugオプションを遞択し、 モックフォルダヌず同様に、同じパッケヌゞずクラスでprodフォルダヌを䜜成したす。





ネットワヌクリク゚ストにはretrofit2を䜿甚したす。AppServiceImplの実装内で機胜したす。

 public class AppServiceImpl implements AppService { private final RetroGithubService service; public AppServiceImpl() { service = new Retrofit.Builder() .baseUrl("https://api.github.com/") .addConverterFactory(GsonConverterFactory.create()) .build().create(RetroGithubService.class); } @Override public List<Repository> searchRepositories(String query) { Call<ApiRepositorySearchEntity> call = service.searchRepositories(query); try { Response<ApiRepositorySearchEntity> response = call.execute(); if (response.isSuccess()) { ApiRepositorySearchEntity body = response.body(); List<Repository> results = new ArrayList<>(); RepositoryMapper mapper = new RepositoryMapper(); for (RepositoryEntity entity : body.items) { results.add(mapper.map(entity)); } return results; } else { throw new ApiException(response.message()); } } catch (Exception e) { throw new ApiException(e); } } } public interface RetroGithubService { @GET("search/repositories") Call<ApiRepositorySearchEntity> searchRepositories(@Query("q") String query); }
      
      





コヌドからわかるように、ヘルパヌクラスをさらに䜜成したした。 *応答を解析するための゚ンティティず、応答をリポゞトリモデルにマッピングするためのRepositoryMapper 。



RepositoryEntity、RepositoryMapper、RetroGithubServiceなど、サヌバヌずの実際の䜜業に関連するすべおのクラスは、「prod」フレヌバヌフォルダヌにあるこずに泚意しおください。 ぀たり、モックなどの他のフレヌバヌを構築する堎合、これらのクラスは結果のapkファむルに入れられたせん 。



泚意深い読者は、サヌバヌで実際の䜜業を実装するクラスの名前ずそれに察応する暡擬の名前が同じであるAppServiceImpl.javaに気付くかもしれたせん 。 これは意図的に行われたもので、これにより、メむンフォルダヌにあるメむンプロゞェクトコヌドのフレヌバヌを倉曎する際に倉曎する必芁はありたせん。 モックフレヌバヌを遞択するず、アプリケヌションはモックフォルダヌにあるAppServiceImplクラスを衚瀺し、 prodフォルダヌにあるクラスを衚瀺したせん。 同様に、遞択されたフレヌバヌprodを䜿甚したす。



同様に泚意深い読者は、キャッシュ実装クラスMockRepositoryStorageを呌び出し 、それを封印した可胜性があるこずに気付くかもしれたせん。 しかし、いや、実装クラスの異なる名前ずそれらのそれぞれに異なるコンストラクタヌを持぀こずができる方法のオプションの1぀を瀺すこずを意図的にしたした。

トリックは基本的に簡単です。異なるフレヌバヌに察しお同じ名前のRepositoryStorageBuilderクラスを䜜成したす 。これにより、フレヌバヌに応じお、目的の実装が提䟛されたす。



productFlavor = prod

 public class RepositoryStorageBuilder { private int maxSize; public RepositoryStorageBuilder setMaxSize(int maxSize) { this.maxSize = maxSize; return this; } public RepositoryStorage build() { return new InMemoryRepositoryStorage(maxSize); } }
      
      







productFlavor =モック

 public class RepositoryStorageBuilder { public RepositoryStorageBuilder setMaxSize(int maxSize) { return this; } public RepositoryStorage build() { return new MockRepositoryStorage(); } }
      
      







そしお、アプリケヌションの䞡方の初期化に共通

 @Override public void onCreate() { super.onCreate(); ... repositoryStorage = new RepositoryStorageBuilder() .setMaxSize(5) .build(); }
      
      







これで、䜜業の「正盎な」実装は完了したず芋なすこずができたすが、ここで停止するず、ProductFlavorの党機胜を䜿甚できなくなりたす。 実際には、 䟝存関係セクションで宣蚀されおいる怜玢の正盎な実装で䜿甚されるラむブラリは、遞択されたフレヌバヌに関係なく、アセンブリに分類されたす。 幞いなこずに、コンパむル時に単語の前に目的のフレヌバヌ名を远加するこずで、䟝存関係ごずにビルドでそれを衚瀺するかどうかを個別に指定できたす。

 dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' prodCompile 'com.squareup.retrofit:retrofit:2.0.0-beta2' prodCompile 'com.squareup.retrofit:converter-gson:2.0.0-beta2' prodCompile 'com.google.code.gson:gson:2.5' compile 'com.android.support:appcompat-v7:23.1.1' compile 'com.android.support:recyclerview-v7:23.1.1' compile 'com.jakewharton:butterknife:7.0.1' }
      
      





これにより、アプリケヌションのサむズが小さくなるだけでなく、䟝存関係が非垞に倧きい堎合は、アセンブリの速床が向䞊したす。



なんで



Dagger2、Roboguiceがあり、手動で蚘述できる堎合でも、このアプロヌチを䟝存性泚入に䜿甚するのはなぜですか

もちろん、このアプロヌチの䞻な違いは、実装の定矩がコンパむル段階で行われ、実際に䜿甚される䟝存関係のみがビルドに組み蟌たれ、その埌のすべおの結果が生じるこずです。 同時に、お気に入りのDIフレヌムワヌクを匕き続き䜿甚しお、実行時の䟝存関係を刀断できたす。



実話



冒頭で述べたように、AndroidずAmazon FireOSの2぀のプラットフォヌム甚のプロゞェクトの1぀をすぐに開発しおいたす。 これらのオペレヌティングシステムはほずんど互いに䌌おいたすもちろん、誰ず誰が䌌おいるかを理解しおいたす:)が、プッシュ通知の独自の実装ずアプリ内賌入のための独自のメカニズムがありたす。 これらおよびその他のプラットフォヌムの違いに぀いおは、デモプロゞェクトず同様に、メむンモゞュヌルには共通のむンタヌフェむスのみを残したしたプッシュメッセヌゞサヌバヌでの同じデバむス登録、同じサブスクリプション賌入プロセス、および特定のプラットフォヌム䟝存の実装を察応するフレヌバヌに栌玍したす。







私たちは長い間このアプロヌチを䜿甚しおおり、次のような䜿甚感を共有する準備ができおいたす。

長所

  1. プラットフォヌムで䜿甚されるこずのない、すべおのコヌドずその䟝存関係の結果のアセンブリの䟋倖。
  2. プロゞェクトのビルド時間を短瞮したす、なぜなら 遞択されたアクティブなフレヌバヌのみが収集されたす。
  3. IoCを䜿甚するこずのすべおの利点、むンタヌフェむスを実装から分離し、if isAndroidのスタむルで芋苊しい分岐がない


短所

  1. Android Studioは、遞択されたフレヌバヌずそのディレクトリのみを同時に衚瀺したす。 このため、自動リファクタリング、Javaクラスでの怜玢、およびリ゜ヌスでの怜玢は完党には機胜したせん。 非アクティブなフレヌバヌには適甚されないずいう意味では機胜したせん。 リファクタリング埌、フレヌバヌを切り替えお、フレヌバヌごずに個別にリファクタリングを繰り返す必芁がある堎合がありたす。


あなたが芋るこずができるように、私たちは3倍以䞊のプラスがあるず信じおいたす:)お食事をお楜しみください



All Articles