ãã®èšäºã§ã¯ãRxJavaãšRetrofitã䜿çšããŠãMVPãã¿ãŒã³ã«åºã¥ããã¢ãŒããã¯ãã£èšèšãšã¢ãã€ã«ã¢ããªã±ãŒã·ã§ã³ã®äœæã«ã€ããŠèª¬æããŸãã ãããã¯ã¯éåžžã«å€§ããããšãå€æãããããå¥ã ã®éšåã§æäŸãããŸãïŒæåã¯ã¢ããªã±ãŒã·ã§ã³ãèšèšããã³äœæãã2çªç®ã¯Dagger 2ã§DIãå®è¡ããŠåäœãã¹ããäœæãã3çªç®ã§ã¯çµ±åããã³æ©èœãã¹ããè¿œå ããAndroidéçºã®çŸå®ã®TDDã«ã€ããŠãæ€èšããŸãã
å 容ïŒ
- ã¯ããã«
- ã¹ããã1.ã·ã³ãã«ãªã¢ãŒããã¯ãã£
- ã¹ããã2.è€éãªã¢ãŒããã¯ãã£
- çµè«ãŸãã¯ç¶ç¶ãã
å®å
šãªã³ã³ãã³ã
- ã¯ããã«
- ã¹ããã1.ã·ã³ãã«ãªã¢ãŒããã¯ãã£
- ã¹ããã2.è€éãªã¢ãŒããã¯ãã£
- ã¹ããã3.äŸåæ§æ³šå ¥
- ã¹ããã4.ãŠããããã¹ã
- ã¹ããã5.çµ±åãã¹ã
- ã¹ããã6.æ©èœãã¹ã
- ã¹ããã7. TDD
- ã¹ããã8.次ã¯äœã§ããïŒ
- ãããã«
ã¯ããã«
ã³ãŒãã®ç解ãæ·±ããé 次è€éã«ããããã«ãèšèšã2ã€ã®æ®µéã«åå²ããŸããããªããã£ãïŒæå°éå®è¡å¯èœïŒãšåŸæ¥ã®ã¢ãŒããã¯ãã£ã§ãã ããªããã£ãã§ã¯ãæå°éã®ã³ãŒããšãã¡ã€ã«ã管çããŠããããã®ã³ãŒããæ¹åããŸãã
ãã¹ãŠã®ãœãŒã¹ã³ãŒãã¯githubã«ãããŸãã ãªããžããªã®ãã©ã³ãã¯ãèšäºã®ã¹ãããã«å¯Ÿå¿ããŠããŸãã ã¹ããã1åçŽãªã¢ãŒããã¯ã㣠-æåã®ã¹ãããã ã¹ããã2è€éãªã¢ãŒããã¯ãã£-2çªç®ã®ã¹ãããã
ããšãã°ãGithub APIã䜿çšããŠãç¹å®ã®ãŠãŒã¶ãŒã®ãªããžããªã®ãªã¹ããååŸããŠã¿ãŸãããã
ãã®ã¢ããªã±ãŒã·ã§ã³ã§ã¯ãRxã䜿çšããŠããããããã®èšäºãç解ããã«ã¯ããã®ãã¯ãããžãŒã®äžè¬çãªèãæ¹ãå¿ èŠã§ããGrokaiRxJavaã®äžé£ã®åºçç©ãèªãããšããå§ãããŸãã
ã¹ããã1.ã·ã³ãã«ãªã¢ãŒããã¯ãã£
éå±€åãMVP
ã¢ãŒããã¯ãã£ãèšèšãããšãã¯ãMVPãã¿ãŒã³ãé å®ããŸãã 詳现ã¯ãã¡ããã芧ãã ããïŒ
https://ru.wikipedia.org/wiki/Model-View-Presenter
http://habrahabr.ru/post/131446/
ããã°ã©ã å šäœã3ã€ã®äž»èŠãªå±€ã«åå²ããŸãã
ã¢ãã« -ããã§ããŒã¿ãååŸããŠä¿åããŸãã åºåã¯èŠ³æž¬å¯èœã§ãã
ãã¬ãŒã³ã¿ãŒ -ãã®ã¬ã€ã€ãŒã«ã¯ããã¹ãŠã®ã¢ããªã±ãŒã·ã§ã³ããžãã¯ãæ ŒçŽãããŸãã ObservableãååŸãããµãã¹ã¯ã©ã€ãããçµæããã¥ãŒã«æž¡ããŸãã
ãã¥ãŒ -衚瀺ã¬ã€ã€ãŒããã¹ãŠã®ãã¥ãŒèŠçŽ ãã¢ã¯ãã£ããŒã·ã§ã³ããã©ã°ã¡ã³ããªã©ãå«ãŸããŸãã
æ¡ä»¶ä»ãã¹ããŒã ïŒ
ã¢ãã«
ããŒã¿ã¬ã€ã€ãŒã¯ãObservable <List <Repo >>ãæäŸããå¿ èŠããããŸããã€ã³ã¿ãŒãã§ã€ã¹ãäœæããŸãããã
public interface Model { Observable<List<Repo>> getRepoList(String name); }
åŸä»ã
ãããã¯ãŒã¯ã§ã®äœæ¥ãç°¡çŽ åããããã«ãRetrofitã䜿çšããŸãã Retrofitã¯REST APIãæäœããããã®ã©ã€ãã©ãªã§ãããããã¯ãŒã¯ã§ã®ãã¹ãŠã®äœæ¥ãåŠçããŸããã€ã³ã¿ãŒãã§ã€ã¹ãšæ³šéã䜿çšããŠãªã¯ãšã¹ããèšè¿°ããããšããã§ããŸããã
ã¬ãããã£ãã2
Runetã®ã¬ãããã£ããã«é¢ããè³æã¯å€æ°ãããŸãïŒ http://www.pvsm.ru/android/58484ãhttp://tttzof351.blogspot.ru/2014/01/java-retrofit.html ïŒã
2çªç®ã®ããŒãžã§ã³ãšæåã®ããŒãžã§ã³ã®äž»ãªéãã¯ãåæã¡ãœãããšéåæã¡ãœããã®éãããªããªã£ãããšã§ãã ããã§Call <Data>ãååŸããŸããããã¯ãåæèŠæ±ã®å Žåã¯executeïŒïŒããéåæèŠæ±ã®å Žåã¯executeïŒã³ãŒã«ããã¯ïŒãåŒã³åºãããšãã§ããŸãã ãŸãããªã¯ãšã¹ãããã£ã³ã»ã«ããåŸ æã®æ©äŒããããŸããïŒcall.cancelïŒïŒã 以åãšåæ§ã«ãç¹å¥ãªãã©ã°ã€ã³ã®å©ããåããŠãObservable <Data>ãååŸã§ããŸã
2çªç®ã®ããŒãžã§ã³ãšæåã®ããŒãžã§ã³ã®äž»ãªéãã¯ãåæã¡ãœãããšéåæã¡ãœããã®éãããªããªã£ãããšã§ãã ããã§Call <Data>ãååŸããŸããããã¯ãåæèŠæ±ã®å Žåã¯executeïŒïŒããéåæèŠæ±ã®å Žåã¯executeïŒã³ãŒã«ããã¯ïŒãåŒã³åºãããšãã§ããŸãã ãŸãããªã¯ãšã¹ãããã£ã³ã»ã«ããåŸ æã®æ©äŒããããŸããïŒcall.cancelïŒïŒã 以åãšåæ§ã«ãç¹å¥ãªãã©ã°ã€ã³ã®å©ããåããŠãObservable <Data>ãååŸã§ããŸã
ãªããžããªããŒã¿ãååŸããããã®ã€ã³ã¿ãŒãã§ã€ã¹ïŒ
public interface ApiInterface { @GET("users/{user}/repos") Observable<List<Repo>> getRepositories(@Path("user") String user); }
ã¢ãã«ã®å®è£
public class ModelImpl implements Model { ApiInterface apiInterface = ApiModule.getApiInterface(); @Override public Observable<List<Repo>> getRepoList(String name) { return apiInterface.getRepositories(name) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); } }
ããŒã¿ã®æäœãPOJO
ã¬ãããã£ããïŒããã³ãã®äžã®GSONïŒã¯ãPOJOïŒPlain Old Java ObjectïŒã§åäœããŸãã ããã¯ã次ã®åœ¢åŒã®JSONãããªããžã§ã¯ããååŸããããšãæå³ããŸãã
{ "id":3, "name":"Andrey", "phone":"511 55 55" }
GSONãå€ãæžã蟌ãUserã¯ã©ã¹ãå¿ èŠã«ãªããŸãã
public class User { private int id; private String name; private String phone; public int getId() { return id; } public void setId(int id) { this.id = id; } // etc }
ãã®ãããªã¯ã©ã¹ãèªåã®æã§çæããå¿ èŠã¯åœç¶ãããŸãã;ããã«ã¯ãäŸãã°www.jsonschema2pojo.orgã®ãããªç¹å¥ãªãžã§ãã¬ãŒã¿ãŒããããŸãã
JSONããã£ãŒãããéžæããŸãïŒ
ãœãŒã¹ã¿ã€ãïŒJSON
泚éã¹ã¿ã€ã«ïŒGson
ã²ãã¿ãŒãšã»ãã¿ãŒãå«ãã
ãã¡ã€ã«ã®ã³ãŒããååŸããŸãã zipãŸãã¯jarãšããŠããŠã³ããŒãããŠããããžã§ã¯ãã«å ¥ããããšãã§ããŸãã ãªããžããªãŒã«ã€ããŠã¯ãææè ãèš±å¯ããªããžããªãŒã®3ã€ã®ãªããžã§ã¯ããååŸãããŸããã
ãµã³ãã«çæã³ãŒã
public class Permissions { @SerializedName("admin") @Expose private boolean admin; @SerializedName("push") @Expose private boolean push; @SerializedName("pull") @Expose private boolean pull; /** * @return The admin */ public boolean isAdmin() { return admin; } /** * @param admin The admin */ public void setAdmin(boolean admin) { this.admin = admin; } /** * @return The push */ public boolean isPush() { return push; } /** * @param push The push */ public void setPush(boolean push) { this.push = push; } /** * @return The pull */ public boolean isPull() { return pull; } /** * @param pull The pull */ public void setPull(boolean pull) { this.pull = pull; } }
çºè¡šè
ãã¬ãŒã³ã¿ãŒã¯ãããŠã³ããŒãããå 容ããšã©ãŒãçºçããå Žåã®å¯ŸåŠæ¹æ³ãªã©ãç¥ã£ãŠããŸãã ã€ãŸããããžãã¯ããã¬ãŒã³ããŒã·ã§ã³ããåé¢ããŸãã ãã®å Žåã®è¡šç€ºã¯ãå¯èœãªéãç°¡åã§ããããšãããããŸããã ãã¬ãŒã³ã¿ãŒã¯ãæ€çŽ¢ãã¿ã³ã®ã¯ãªãã¯ãåŠçããããŠã³ããŒããåæåããããŒã¿ãéä¿¡ããã¢ã¯ãã£ããã£ãåæ¢ããå Žåã«ç»é²ã解é€ã§ããå¿ èŠããããŸãã
ãã¬ãŒã³ã¿ãŒã€ã³ã¿ãŒãã§ã€ã¹ïŒ
public interface Presenter { void onSearchClick(); void onStop(); }
ãã¬ãŒã³ã¿ãŒã®å®è£
public class RepoListPresenter implements Presenter { private Model model = new ModelImpl(); private View view; private Subscription subscription = Subscriptions.empty(); public RepoListPresenter(View view) { this.view = view; } @Override public void onSearchButtonClick() { if (!subscription.isUnsubscribed()) { subscription.unsubscribe(); } subscription = model.getRepoList(view.getUserName()) .subscribe(new Observer<List<Repo>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { view.showError(e.getMessage()); } @Override public void onNext(List<Repo> data) { if (data != null && !data.isEmpty()) { view.showData(data); } else { view.showEmptyList(); } } }); } @Override public void onStop() { if (!subscription.isUnsubscribed()) { subscription.unsubscribe(); } } }
衚瀺ãã
Viewã¯ãåä¿¡ããããŒã¿ã®è¡šç€ºããšã©ãŒã®è¡šç€ºã空ã®ãªã¹ãã®éç¥ããã¬ãŒã³ã¿ãŒããã®ãªã¯ãšã¹ãã«å¿ãããŠãŒã¶ãŒåã®è¡šç€ºãå¯èœãªã¢ã¯ãã£ããã£ãšããŠViewãå®è£ ããŸãã ã€ã³ã¿ãŒãã§ãŒã¹ïŒ
public interface IView { void showList(List<Repo> RepoList); void showError(String error); void showEmptyList(); String getUserName(); }
ã¡ãœããå®è£
ã®è¡šç€º
@Override public void showData(List<Repo> list) { adapter.setRepoList(list); } @Override protected void onStop() { super.onStop(); if (presenter != null) { presenter.onStop(); } } @Override public void showError(String error) { makeToast(error); } @Override public void showEmptyList() { makeToast(getString(R.string.empty_repo_list)); } @Override public String getUserName() { return editText.getText().toString(); }
ãã®çµæãã¬ã€ã€ãŒã«åå²ãããã·ã³ãã«ãªã¢ããªã±ãŒã·ã§ã³ãã§ããŸããã
ã¹ããŒã ïŒ
æ¹åãå¿ èŠãªãã®ããããŸãããäžè¬çãªèãæ¹ã¯æ確ã§ãã 次ã«ãæ°ããæ©èœãè¿œå ããŠã¿ã¹ã¯ãè€éã«ããŸãã
ããŒã2.è€éãªã¢ãŒããã¯ãã£
ã¢ããªã±ãŒã·ã§ã³ã«æ°ããæ©èœãè¿œå ãããªããžããªã«é¢ããæ å ±ã衚瀺ããŸãã ãã©ã³ããšè²¢ç®è ã®ãªã¹ãã衚瀺ããŸãããããã¯APIããã®ããŸããŸãªãªã¯ãšã¹ãã«ãã£ãŠååŸãããŸãã
ã¬ããã©ã ã
ã©ã ããªãã§Rxã䜿çšããã®ã¯èŠçã§ãããã«ç²ãããã³ã«å¿åã¯ã©ã¹ãæžãå¿ èŠããããŸãã Androidã¯Java 8ãšã©ã ãããµããŒãããŠããŸããããRetrolambdaïŒ https://github.com/evant/gradle-retrolambda ïŒãå©ãã«ãªããŸãã ã©ã ãåŒã®è©³çŽ°ïŒ http : //habrahabr.ru/post/224593/
ã¬ã€ã€ãŒããšã«ç°ãªãããŒã¿ã¢ãã«ã
ã芧ã®ãšããã3ã€ã®ã¬ã€ã€ãŒãã¹ãŠã§åãRepoããŒã¿ãªããžã§ã¯ãã䜿çšããŠããŸãã ãã®ã¢ãããŒãã¯åçŽãªã¢ããªã±ãŒã·ã§ã³ã«ã¯é©ããŠããŸãããå®éã«ã¯ãAPIã®å€æŽããªããžã§ã¯ããå€æŽããå¿ èŠæ§ããŸãã¯äœãä»ã®ãã®ã«åºãããããšããããŸãã è€æ°ã®äººããããžã§ã¯ãã«åãçµãã§ããå Žåãå¥ã®ã¬ã€ã€ãŒã®å©çã®ããã«ã¯ã©ã¹ãå€æŽãããªã¹ã¯ããããŸãã
ãã®ãããå€ãã®å Žåã1ã€ã®ã¬ã€ã€ãŒ= 1ã€ã®ããŒã¿åœ¢åŒãšããã¢ãããŒããé©çšãããŸãã ã¢ãã«ã®äžéšã®ãã£ãŒã«ããå€æŽãããŠããããã¯ãã¥ãŒã¬ã€ã€ãŒã«åœ±é¿ããŸããã Presenterã¬ã€ã€ãŒã«å€æŽãå ããããšã¯ã§ããŸãããViewã§ã¯å³å¯ã«å®çŸ©ããããªããžã§ã¯ãïŒã¯ã©ã¹ïŒãæäŸããŸãã ãã®ãããããŒã¿ã¢ãã«ããã®ã¬ã€ã€ãŒã®ç¬ç«æ§ãéæãããåã¬ã€ã€ãŒã«ã¯ç¬èªã®ã¢ãã«ããããŸãã ã¢ãã«ãå€æŽãããšãã¯ãããããŒãæžãæããŠãã¬ã€ã€ãŒèªäœã«ã¯è§Šããªãããã«ããå¿ èŠããããŸãã ããã¯ã³ã³ãã©ã¯ãããã°ã©ãã³ã°ã«äŒŒãŠããŸããã©ã®ãªããžã§ã¯ããã¬ã€ã€ãŒã«å°éããã©ã®ãªããžã§ã¯ããããã«æäŸããå¿ èŠãããããæ£ç¢ºã«ææ¡ããŠãããããäºæž¬äžå¯èœãªçµæããèªåèªèº«ãšååãä¿è·ããŸãã
ãã®äŸã§ã¯ãDTO-ããŒã¿è»¢éãªããžã§ã¯ãïŒJSONãªããžã§ã¯ããå®å šã«ã³ããŒïŒãšãã¥ãŒãªããžã§ã¯ãïŒè¡šç€ºçšã«èª¿æŽããããªããžã§ã¯ãïŒã®2çš®é¡ã®ããŒã¿ã§ååã§ãã ããè€éãªã¢ããªã±ãŒã·ã§ã³ãããå Žåãããžãã¹ãªããžã§ã¯ãïŒããžãã¹ããã»ã¹çšïŒãŸãã¯ããŒã¿ããŒã¹ãªããžã§ã¯ãïŒããŒã¿ããŒã¹ã«è€éãªãªããžã§ã¯ããä¿åããããïŒãå¿ èŠã«ãªãå ŽåããããŸã
éä¿¡ãããããŒã¿ã®æŠç¥å³
ãªããžããªã®ååãRepositoryDTOã«å€æŽããæ°ãããªããžããªã¯ã©ã¹ãäœæããã€ã³ã¿ãŒãã§ã€ã¹Func1 <List <RepositoryDTO >>ãList <Repository >>ãå®è£ ããããããŒãèšè¿°ããŸãã
ïŒãªã¹ã<RepositoryDTO>ãããªã¹ã<Repository>ãžã®ç¿»èš³ïŒ
ãªããžã§ã¯ãã®ããããŒ
public class RepoBranchesMapper implements Func1<List<BranchDTO>, List<Branch>> { @Override public List<Branch> call(List<BranchDTO> branchDTOs) { List<Branch> branches = Observable.from(branchDTOs) .map(branchDTO -> new Branch(branchDTO.getName())) .toList() .toBlocking() .first(); return branches; } }
ã¢ãã«
ã¬ã€ã€ãŒããšã«ç°ãªãããŒã¿ã¢ãã«ãå°å ¥ããŸãããModelã€ã³ã¿ãŒãã§ã€ã¹ã¯DTOãªããžã§ã¯ããæäŸããããã«ãªããŸããããã以å€ã¯ãã¹ãŠåãã§ãã
public interface Model { Observable<List<RepositoryDTO>> getRepoList(String name); Observable<List<BranchDTO>> getRepoBranches(String owner, String name); Observable<List<ContributorDTO>> getRepoContributors(String owner, String name); }
çºè¡šè
Presenterã¬ã€ã€ãŒã§ã¯ãå ±éã®ã¯ã©ã¹ãå¿ èŠã§ãã ãã¬ãŒã³ã¿ãŒã¯ãããŸããŸãªæ©èœãå®è¡ã§ããŸããããŠã³ããŒãã·ã§ãŒã®ç°¡åãªãã¬ãŒã³ã¿ãŒãã¢ã€ãã ãããŒãããå¿ èŠããããªã¹ãããµã€ãäžã®ãªããžã§ã¯ãããªã¯ãšã¹ãããããããããã³ä»ã®å€ãã®ãšã³ãã£ãã£ããããŸãã ãããããããã¯ãã¹ãŠãã¡ã¢ãªãªãŒã¯ãåé¿ããããã«Observableãããµãã¹ã¯ã©ã€ãã解é€ããå¿ èŠæ§ã«ãã£ãŠçµ±äžãããŠããŸãã æ®ãã¯çºè¡šè ã®ã¿ã€ãã«ãã£ãŠç°ãªããŸãã
è€æ°ã®Observableã䜿çšããå ŽåãonStopã§äžåºŠã«ãã¹ãŠã®ãµãã¹ã¯ãªãã·ã§ã³ã解é€ããå¿ èŠããããŸãã ãããè¡ãã«ã¯ãCompositeSubscriptionã䜿çšã§ããŸãããã¹ãŠã®ãµãã¹ã¯ãªãã·ã§ã³ãããã«è¿œå ããã³ãã³ãã§ãµãã¹ã¯ã©ã€ãã解é€ããŸãã
ãŸãããã¬ãŒã³ã¿ãŒã«ç¶æ ã®ä¿åãè¿œå ããŸãã ãããè¡ãã«ã¯ãonCreateïŒBundledInstanceStateïŒããã³onSaveInstanceStateïŒBundle outStateïŒã¡ãœãããäœæããŠå®è£ ããŸãã DTOãVOã«å€æããã«ã¯ãããããŒã䜿çšããŸãã
ã³ãŒãäŸ
public void onSearchButtonClick() { String name = view.getUserName(); if (TextUtils.isEmpty(name)) return; Subscription subscription = dataRepository.getRepoList(name) .map(repoListMapper) .subscribe(new Observer<List<Repository>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { view.showError(e.getMessage()); } @Override public void onNext(List<Repository> list) { if (list != null && !list.isEmpty()) { repoList = list; view.showRepoList(list); } else { view.showEmptyList(); } } }); addSubscription(subscription); } public void onCreate(Bundle savedInstanceState) { if (savedInstanceState != null) { repoList = (List<Repository>) savedInstanceState.getSerializable(BUNDLE_REPO_LIST_KEY); if (!isRepoListEmpty()) { view.showRepoList(repoList); } } } private boolean isRepoListEmpty() { return repoList == null || repoList.isEmpty(); } public void onSaveInstanceState(Bundle outState) { if (!isRepoListEmpty()) { outState.putSerializable(BUNDLE_REPO_LIST_KEY, new ArrayList<>(repoList)); } }
ãã¬ãŒã³ã¿ãŒã¬ã€ã€ãŒã®äžè¬çãªã¹ããŒã ïŒ
衚瀺ãã
ã¢ã¯ãã£ããã£ã䜿çšããŠãã©ã°ã¡ã³ãã管çããŸãã åãšã³ãã£ãã£ã«ã¯ç¬èªã®ãã©ã°ã¡ã³ãããããããŒã¹ãã©ã°ã¡ã³ãããç¶æ¿ãããŸãã ããŒã¹ãã¬ãŒã³ã¿ãŒã€ã³ã¿ãŒãã§ã€ã¹ã䜿çšããããŒã¹ãã©ã°ã¡ã³ãã¯ãonStopïŒïŒã§ãµãã¹ã¯ã©ã€ã解é€ãããŸãã
ãŸããç¶æ ã®åŸ©å ã«ã泚æããŠãã ããããã¹ãŠã®ããžãã¯ããã¬ãŒã³ã¿ãŒã«ç§»åããŸããããã¥ãŒã¯ã§ããã ãã·ã³ãã«ã«ããå¿ èŠããããŸãã
åºæ¬ãã©ã°ã¡ã³ãã³ãŒã
@Override public void onStop() { super.onStop(); if (getPresenter() != null) { getPresenter().onStop(); } }
ãã¥ãŒã¬ã€ã€ãŒã®äžè¬çãªã¬ã€ã¢ãŠã
2çªç®ã®ã¹ãããã§ã®ã¢ããªã±ãŒã·ã§ã³ã®äžè¬çãªã¹ããŒã ïŒ ã¯ãªãã¯å¯èœ ïŒïŒ
çµè«ãŸãã¯ç¶ç¶...
ãã®çµæãæœè±¡åã®å¿ èŠãªãã¹ãŠã®ã¬ãã«ãšã³ã³ããŒãã³ãïŒ ãœãŒã¹ ïŒã«ãã責任ã®æ確ãªåå²ã«æºæ ããå®çšçãªã¢ããªã±ãŒã·ã§ã³ãæã«å ¥ããŸããã ãã®ãããªã³ãŒãã¯ä¿å®ãšè£å®ã容æã§ãããéçºè ã®ããŒã ãããã«åãçµãããšãã§ããŸãã ããããäž»ãªå©ç¹ã®1ã€ã¯éåžžã«ç°¡åãªãã¹ãã§ãã 次ã®èšäºã§ã¯ãDagger 2ã®å°å ¥ãæ€èšããTDDã®ååã«åŸã£ãŠãæ¢åã®ã³ãŒãããã¹ãã§ã«ããŒããæ°ããæ©èœãäœæããŸãã
æŽæ°
Androidã¢ããªã段éçã«æ§ç¯ãããããŒã2
Androidã¢ããªã段éçã«æ§ç¯ãããããŒã3