æè¿Androidçéã§ã¯MVPã¨ããè¨èãããèãæ°ããã¾ãã
å人çã«ãæ°ã«ãªã£ã¦ãã¦ãç¹ã«ããã¹ããæ¸ãããããªããã¨ããé¨åãã¨ã¦ãæ°ã«ãªãã¾ãã
ã¨ãããã©ãããã¹ããæ¸ãããããããªãµã³ãã«ã³ã¼ãããªããªãè¦ã¤ãããªãâ¦ã
ããã§ãå
¨ç¶ãã¹ããæ¸ãã¦ããªãAndroidéçºè
ã®ä¸äººã¨ãã¦ãæ¬å½ã«UIãã¸ãã¯ã®ãã¹ããå¯è½ãªãã®ãªã®ããå®éã«MVPã£ã½ããã®ãæ¸ãã¦è©¦ãã¦ã¿ã¾ããã
ã¡ãªã¿ã«åã¯ã¦ããããã¹ãã®ç¥è¦ãä¸åããã¾ããã
MVP
Model View Presenterã®äºã§ãã
詳細ã¯åãã¾ã ããã¾ã§ã¡ããã¨ç解åºæ¥ã¦ããªãã®ã§ãä¸è¨ã®è¨äºãªã©ãåç
§ãã¦ãã ããã
ãã£ããè¨ãã¨ãControllerã®ä»£ããã«Presenterã¨ãããã®ä½ããPresenterãUIãã¸ãã¯ãæ
ãäºã§Viewãªã®ãControllerãªã®ãæ±ããææ§ã ã£ãActivityãFragmentãå®å
¨ã«å¤ãåã渡ãã ãã®Viewã¨ãã¦æ±ãåãæ¿ãå¯è½ãªãã®ã«ãããããªæãã®ããã§ãã
ä¸è¨ã®è¨äºã§ã¯ããã«DDDã§ã®è¨è¨ãã¿ã¼ã³ãåãå
¥ããUseCaseãªã©å®ç¾©ãã¦ããããã§ãã
æ¸ãã¦ã¿ã
以åKotlinã®ãµã³ãã«ã§ä½ã£ãGithubã®ã¦ã¼ã¶ã¼æ
å ±ã表示ããã ãã®ã¢ããªãæ¸ãæãã¦ã¿ã¾ããã
github.com
ãããã¡ããKotlinã§ãã
ã¯ã©ã¹æ¦è¦
Presenterãä½ããã¹ããæ¸ãã¨ããã®ãç®çã¨ãã¦æ¸ãã¾ããã
- UserInfoActivityï¼Viewã«ãããActivityã
- IUserInfoActivityï¼UserInfoActivityã®Interfaceã
- UserInfoPresenterï¼Activityã¨UseCaseãæã¡UIãã¸ãã¯ãæ ãã
- UserInfoUseCaseï¼ãã¼ã¿ãåå¾ãããã¸ãã¹ãã¸ãã¯ãæ ãã
- IUserInfoUseCaseï¼UserInfoUseCaseã®Interfaceã
Activity
Activityã«ã¯è¡¨åºå¤å®ãªã©ã®UIãã¸ãã¯ã¯æ¸ãããViewã«å¤ã渡ããPresenterã«ã¤ãã³ãã渡ãã ãã®åå¨ã§ãã
public class UserInfoActivity : AppCompatActivity(), IUserInfoActivity { companion object { fun buildBundle(id: String): Bundle { val bundle = Bundle() bundle.putString("id", id) return bundle } } var presenter: UserInfoPresenter? = null override fun onCreate(savedInstanceState: Bundle?) { super<AppCompatActivity>.onCreate(savedInstanceState) setContentView(R.layout.activity_user_info) presenter = UserInfoPresenter(this, UserInfoUseCase()) presenter!!.onCreate(RequestQueueSingleton.get(getApplicationContext())) } override fun onDestroy() { presenter!!.onDestroy() super<AppCompatActivity>.onDestroy() } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (android.R.id.home == item.getItemId()) { finish() } return super<AppCompatActivity>.onOptionsItemSelected(item) } override public fun initActionBar(id: String) { val actionBar = getSupportActionBar() actionBar.setDisplayHomeAsUpEnabled(true) actionBar.setHomeButtonEnabled(true) actionBar.setTitle(id) } override public fun setParentLayoutVisibility(visibility: Int){ parentLayout.setVisibility(visibility) } override public fun getIdFromBundle(): String { return getIntent().getStringExtra("id") } override public fun setName(name: String) { nameText.setText(name) } override public fun setId(id: String) { idText.setText(id) } override public fun setLocation(location: String) { locationText.setText(location) } override public fun setLocationVisibility(visibility: Int) { locationText.setVisibility(visibility) } override public fun setCompany(company: String) { companyText.setText(company) } override public fun setCompanyVisibility(visibility: Int) { companyText.setVisibility(visibility) } override public fun setLink(link: String) { linkText.setText(link) } override public fun setLinkVisibility(visibility: Int) { linkText.setVisibility(visibility) } override public fun setMail(mail: String) { mailText.setText(mail) } override public fun setMailVisibility(visibility: Int) { mailText.setVisibility(visibility) } override public fun setIcon(url: String) { Picasso.with(this).load(url).fit().into(iconImage) } override public fun addLanguage(languageName: String, languageCount: String, languageStartCount: String) { val inflater = LayoutInflater.from(this); val languageView = inflater.inflate(R.layout.activity_user_info_language, null) as LinearLayout val languageNameText = languageView.findViewById(R.id.userInfoLanguageName) as TextView val languageCountText = languageView.findViewById(R.id.userInfoLanguageCount) as TextView val languageStartCountText = languageView.findViewById(R.id.userInfoLanguageStarCount) as TextView languageNameText.setText(languageName) languageCountText.setText(languageCount) languageStartCountText.setText(languageStartCount) languageLayout.addView(languageView) } override public fun addRepository(iconImageResource: Int, repositoryName: String, starCount: String, language: String, description: String, htmlUrl: String) { val inflater = LayoutInflater.from(this); val repositoryView = inflater.inflate(R.layout.activity_user_info_repositories, null) as LinearLayout val repositoryIconImage = repositoryView.findViewById(R.id.userInfoRepositoryIcon) as ImageView val repositoryNameText = repositoryView.findViewById(R.id.userInfoRepositoryName) as TextView val repositoryDescriptionText = repositoryView.findViewById(R.id.userInfoRepositoryDescription) as TextView val repositoryStarCountText = repositoryView.findViewById(R.id.userInfoRepositoryStarCount) as TextView val repositoryLanguageText = repositoryView.findViewById(R.id.userInfoRepositoryLanguage) as TextView repositoryIconImage.setImageResource(iconImageResource) repositoryNameText.setText(repositoryName) repositoryStarCountText.setText(starCount) repositoryLanguageText.setText(language) repositoryDescriptionText.setText(description) repositoryView.findViewById(R.id.userInfoRepositoryBrowser).setOnClickListener { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(htmlUrl))) } repositoryLayout.addView(repositoryView) } override public fun networkErrorHandling() { Toast.makeText(getApplicationContext(), "Error. Username does not exist?", Toast.LENGTH_SHORT).show() finish() } }
ããã¦ãã¡ã½ããã¯å ¨ã¦Interfaceã«å®ç¾©ãã¾ãã
interface IUserInfoActivity { public fun initActionBar(id: String) public fun getIdFromBundle(): String public fun setParentLayoutVisibility(visibility: Int) public fun setName(name: String) public fun setId(id: String) public fun setLocation(location: String) public fun setLocationVisibility(visibility: Int) public fun setCompany(company: String) public fun setCompanyVisibility(visibility: Int) public fun setLink(link: String) public fun setLinkVisibility(visibility: Int) public fun setMail(mail: String) public fun setMailVisibility(visibility: Int) public fun setIcon(url: String) public fun addLanguage(languageName: String, languageCount: String, languageStartCount: String) public fun addRepository(iconImageResource: Int, repositoryName: String, starCount: String, language: String, description: String, htmlUrl: String) public fun networkErrorHandling() }
Presenter
Presenterã¯Activityã¨UseCaseã®Interfaceãæã¡ãActivityã®ã¡ã½ãããå¼ã³åºãäºã§UIãã¸ãã¯ãè¨è¿°ãã¾ãã
public class UserInfoPresenter(val view: IUserInfoActivity, val useCase: IUserInfoUseCase) { private val subscriptions = CompositeSubscription(); public fun onCreate(queue: RequestQueue) { val id = view.getIdFromBundle() view.initActionBar(id) useCase.requestUserInfo ({ pair -> val user = pair.first val repositories = pair.second view.setParentLayoutVisibility(View.VISIBLE) view.setName(user.name) view.setId(user.login) if (isDataEmpty(user.location)) { view.setLocationVisibility(View.GONE) } else { view.setLocation(user.location) view.setLocationVisibility(View.VISIBLE) } if (isDataEmpty(user.company)) { view.setCompanyVisibility(View.GONE) } else { view.setCompany(user.company) view.setCompanyVisibility(View.VISIBLE) } if (isDataEmpty(user.blog)) { view.setLinkVisibility(View.GONE) } else { view.setLink(user.blog) view.setLinkVisibility(View.VISIBLE) } if (isDataEmpty(user.email)) { view.setMailVisibility(View.GONE) } else { view.setMail(user.email) view.setMailVisibility(View.VISIBLE) } if (!isDataEmpty(user.avatarUrl)) { view.setIcon(user.avatarUrl) } useCase.getLanguages(repositories).forEach { language -> view.addLanguage(languageName = language.first, languageCount = language.second.count().toString(), languageStartCount = language.second.map { repo -> repo.stargazersCount }.sum().toString()) } useCase.sortRepositories(repositories).forEach { repository -> val repoIcon = if (repository.fork) R.drawable.ic_call_split_grey600_36dp else android.R.drawable.ic_menu_sort_by_size view.addRepository(iconImageResource = repoIcon, repositoryName = repository.name, starCount = repository.stargazersCount.toString(), language = if (isDataEmpty(repository.language)) "" else repository.language, description = repository.description, htmlUrl = repository.htmlUrl) } }, { e -> view.networkErrorHandling() }, id, queue, subscriptions) } public fun onDestroy() { subscriptions.unsubscribe() } private fun isDataEmpty(data: String): Boolean { return data.equals("null") || data.isEmpty() } }
UseCase
UseCaseã¯Presenterããå¼ã³åºããããã¸ãã¹ãã¸ãã¯ã§ãã
ãã®å ´åã¯Githubã®ã¦ã¼ã¶ã¼æ
å ±ã®åå¾æä½ã§ãããå®éã®å¦çã¯æ´ã«å
·ä½çãªãã¸ãã¯ã¯ã©ã¹ã®UserApiã«ç§»è²ãã¦ãã¾ãã
MVPã«æ¸ãæããåã¯Activityãç´æ¥UserApiã®ã¡ã½ãããå¼ã¶å½¢ã§ããã
public class UserInfoUseCase : IUserInfoUseCase { override fun requestUserInfo(onSuccess: (Pair<User, List<Repository>>) -> Unit, onError: (Throwable) -> Unit, id: String, queue: RequestQueue, subscriptions: CompositeSubscription) { val userRequest = UsersApi.request(id = id, requestQueue = queue) val repositoryRequest = RepositoryApi.request(id = id, page = 1, requestQueue = queue).toList() subscriptions.add(Observable .zip(userRequest, repositoryRequest, { user, repositories -> Pair(user, repositories) }) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ response -> onSuccess(response) }, { e -> onError(e) })) } override fun getLanguages(repositories: List<Repository>): List<Pair<String, List<Repository>>> { return repositories .filter { repo -> !repo.language.equals("null") } .groupBy { repo -> repo.language } .toList().sortDescendingBy { language -> language.second.count() } } override fun sortRepositories(repositories: List<Repository>): List<Repository> { return repositories .sortDescendingBy { repo -> repo.stargazersCount } .sortBy { repo -> repo.fork } } }
UIãã¸ãã¯ã«ãã¹ããæ¸ã
ãã¦ããããããããããæ¬é¡ã§ãã
Presenterã®UIãã¸ãã¯ã«ãã¹ããæ¸ãã¦ã¿ã¾ãã
@RunWith(AndroidJUnit4::class) public class UserInfoPresenterTest { Test public fun setNameTest() { var isSuccess = false val presenter = UserInfoPresenter( object : UserInfoActivityMock() { override fun setName(name: String) { assertThat(name, `is`("name")) isSuccess = true } }, object : UserInfoUseCaseMock(UserMockBuilder.defaultCase()) {}) presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext())) assertTrue(isSuccess) } Test public fun setIdTest() { var isSuccess = false val presenter = UserInfoPresenter( object : UserInfoActivityMock() { override fun setId(id: String) { assertThat(id, `is`("login")) isSuccess = true } }, object : UserInfoUseCaseMock(UserMockBuilder.defaultCase()) {}) presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext())) assertTrue(isSuccess) } Test public fun setLocationTest() { var isSuccess = false val presenter = UserInfoPresenter( object : UserInfoActivityMock() { override fun setLocation(location: String) { assertThat(location, `is`("location")) isSuccess = true } override fun setLocationVisibility(visibility: Int) { assertThat(visibility, `is`(View.VISIBLE)) } }, object : UserInfoUseCaseMock(UserMockBuilder.defaultCase()) {}) presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext())) assertTrue(isSuccess) } Test public fun locationNullCaseTest() { var isSuccess = false val presenter = UserInfoPresenter( object : UserInfoActivityMock() { override fun setLocationVisibility(visibility: Int) { assertThat(visibility, `is`(View.GONE)) isSuccess = true } }, object : UserInfoUseCaseMock(UserMockBuilder.nullCase()) {}) presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext())) assertTrue(isSuccess) } Test public fun setCompanyTest() { var isSuccess = false val presenter = UserInfoPresenter( object : UserInfoActivityMock() { override fun setCompany(company: String) { assertThat(company, `is`("company")) isSuccess = true } override fun setCompanyVisibility(visibility: Int) { assertThat(visibility, `is`(View.VISIBLE)) } }, object : UserInfoUseCaseMock(UserMockBuilder.defaultCase()) {}) presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext())) assertTrue(isSuccess) } Test public fun companyNullCaseTest() { var isSuccess = false val presenter = UserInfoPresenter( object : UserInfoActivityMock() { override fun setCompanyVisibility(visibility: Int) { assertThat(visibility, `is`(View.GONE)) isSuccess = true } }, object : UserInfoUseCaseMock(UserMockBuilder.nullCase()) {}) presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext())) assertTrue(isSuccess) } Test public fun setLinkTest() { var isSuccess = false val presenter = UserInfoPresenter( object : UserInfoActivityMock() { override fun setLink(link: String) { assertThat(link, `is`("blog")) isSuccess = true } override fun setLinkVisibility(visibility: Int) { assertThat(visibility, `is`(View.VISIBLE)) } }, object : UserInfoUseCaseMock(UserMockBuilder.defaultCase()) {}) presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext())) assertTrue(isSuccess) } Test public fun linkNullCaseTest() { var isSuccess = false val presenter = UserInfoPresenter( object : UserInfoActivityMock() { override fun setLinkVisibility(visibility: Int) { assertThat(visibility, `is`(View.GONE)) isSuccess = true } }, object : UserInfoUseCaseMock(UserMockBuilder.nullCase()) {}) presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext())) assertTrue(isSuccess) } Test public fun setMailTest() { var isSuccess = false val presenter = UserInfoPresenter( object : UserInfoActivityMock() { override fun setMail(mail: String) { assertThat(mail, `is`("email")) isSuccess = true } override fun setMailVisibility(visibility: Int) { assertThat(visibility, `is`(View.VISIBLE)) } }, object : UserInfoUseCaseMock(UserMockBuilder.defaultCase()) {}) presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext())) assertTrue(isSuccess) } Test public fun mailNullCaseTest() { var isSuccess = false val presenter = UserInfoPresenter( object : UserInfoActivityMock() { override fun setMailVisibility(visibility: Int) { assertThat(visibility, `is`(View.GONE)) isSuccess = true } }, object : UserInfoUseCaseMock(UserMockBuilder.nullCase()) {}) presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext())) assertTrue(isSuccess) } Test public fun setIconTest(){ var isSuccess = false val presenter = UserInfoPresenter( object : UserInfoActivityMock() { override fun setIcon(url: String) { assertThat(url, `is`("avatar_url")) isSuccess = true } }, object : UserInfoUseCaseMock(UserMockBuilder.defaultCase()) {}) presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext())) assertTrue(isSuccess) } Test public fun iconNullCaseTest(){ val presenter = UserInfoPresenter( object : UserInfoActivityMock() { override fun setIcon(url: String) { fail("") } }, object : UserInfoUseCaseMock(UserMockBuilder.nullCase()) {}) presenter.onCreate(RequestQueueSingleton.get(InstrumentationRegistry.getContext())) } }
åããã«ããã§ãããPresenterã«Activityã¨UseCaseã®ã¢ãã¯å®è£
ã渡ããActivityã®Viewæä½ã¡ã½ãããæ£ããå¼ã³åºããã¦ããããæ¤ç¥ããäºã§UIãã¸ãã¯ããã¹ããã¦ãã¾ãã
Activityã®ã¡ã½ãããç´°ããåããInterfaceã«å®ç¾©ããã®ã¯ãã®ããã§ãã
ææ
å¢ãã§ä¸æ°ã«æ¸ãæããã®ã§è¨è¨ã«ã¯ããªãåèã®ä½å°ãããã¨æãã¾ãããåãã¦Androidã®UIãã¸ãã¯ã«å¯¾ãã¦æå³ã®ããã¬ãã«ã®ãã¹ããæ¸ãã¤ã¡ã¼ã¸ãè¦ããæ°ããã¾ãã
ãã®ç²åº¦ã§ãã¹ããæ¸ãã¦ããã°ãæ¡ä»¶ã«ããå¤ã®æ¸¡ãééããã¡ã½ããã®å¼ã³å¿ãã¨ãã£ããã°ã¬ã¯æ¤ç¥ããäºãåºæ¥ããã§ãã
ãã ãããã®ç²åº¦ã§Activityã«ã¡ã½ãããä½ã£ã¦Interfaceãå®ç¾©ãã¦ãã¹ããæ¸ãã¦â¦ã¨ãã£ã¦ããã®ã¯æ£ç´ããªãåé·ã¨ãããç²¾ç¥çã«ãå·¥æ°çã«ãè¾ããã
ããã¾ã§ç´°ããããªãã¦ãããæ°ããããã©ãActivityã®ã¡ã½ããç²åº¦ãèããã¦ãã¾ãã¨Activityã«UIãã¸ãã¯ãå
¥ãè¾¼ãã§ãã¾ããPresenterã®ãã¹ãããã¾ãæå³ããªããªã£ã¦ãã¾ããâ¦ã
ä»åã¯åå¾ãããã¼ã¿ãåç´ã«ä¸è¦§è¡¨ç¤ºããã ãã®ãµã³ãã«ã ã£ãããä½è¨ã«ä¸æ¯ã£ã½ããåºã¦ãã¾ã£ãã®ãããããªãã
ãã®è¾ºãã¯ãã¹ãã©ã¤ãã©ãªãè¨è¨ã®å·¥å¤«ãªã©ã§ããå°ãã©ãã«ãåºæ¥ãã®ã ãããã
ããã¡ãã£ã¨è²ã
試ãã¦å·¥å¤«ãã¦ã¿ããã