Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

Session 2 l Rainist ӣߧળ

Slide 4

Slide 4 text

Efficient and Testable MVVM pattern Session 2. Android archietecture @omjoonkim with using AAC, Rx, Koin

Slide 5

Slide 5 text

Androidীח ݆਷ ઙܨ੄ ௏٘ ইఃఫ୛о ੓णפ׮.

Slide 6

Slide 6 text

?

Slide 7

Slide 7 text

MVC MVP MVVM MVI VIPER etc…

Slide 8

Slide 8 text

MVC MVP MVVM MVI VIPER etc…

Slide 9

Slide 9 text

MVC MVP MVVM MVI VIPER etc… with CleanArchitecture with RFP(rxJava2) with AAC, Koin with Spek

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

MVVM?

Slide 12

Slide 12 text

why MVVM?

Slide 13

Slide 13 text

?

Slide 14

Slide 14 text

Presenter codeо 1000Lineਸ ֈযо ࠄ ੸ ࠺तೠ ചݶীࢲ ઺ࠂغয ࢤࢿغח ௏٘ܳ ࠄ ੸ ݆ࣻ਷ ࢚క(ݯߡ ߸ࣻ)ܳ ҙܻೞ׮ ݠܻо ই౵ ࠄ ੸ View৬ Presenter੄ ࠁੌ۞ ೒ۨ੉౟ ௏٘о טযա ೖҌ೮؍ ੸ ਃҳࢎ೦☝ Business Logic ☝ ௏٘۝ ☝ ࠂ੟ࢿ ☝ ਬ૑ࠁࣻࢿ పझ౟ ਊ੉ࢿ

Slide 15

Slide 15 text

what MVVM?

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

MVVM…. - Model View ViewModel۽ ҳࢿغয ੓ח ಁఢ - ViewModel਷ View੄ ୶࢚ച੉׮. - View৬ ViewModel਷ n:m੄ ҙ҅੉׮. - Viewח ViewModelী bindableೞ׮.

Slide 18

Slide 18 text

Model View ViewModel۽ ҳࢿغয ੓ח ಁఢ VIEW VIEW MODEL MODEL DataBinding Notification Update Notification

Slide 19

Slide 19 text

ViewModel਷ View੄ ୶࢚ച੉׮. - ViewModel਷ View੄ ࢚క৬ ೯ز੉ ୶࢚ച ػ Ѫ੉׮. - ViewModel਷ View੄ inputҗ output੉ ݺदغয ੓ח ੋఠಕ੉झ׮. - ݃஖ ࣽࣻೣࣻ୊ۢ э਷ ч੄ inputীח ೦࢚ э਷ ч੄ output੉ ߈ജ غযঠ ೠ׮. - ૊ ࢎਊ੗੄ ೯ز(input)ী ٮۄ ೦࢚ э਷ Ѿҗ(output)о աఋաѱ ػ׮. - ೯ਤী ٮܲ Ѿҗܳ పझ౟ೞחؘ ਊ੉ೞ׮. - output਷ View੄ ࢚క৬ Route۽ ա׋׮.

Slide 20

Slide 20 text

View৬ ViewModel਷ n:m੄ ҙ҅੉׮. - ೞա੄ Viewо ৈ۞ ViewModelী ઑ೤ ؼ ࣻ ੓׮. - ೞա੄ ViewModel੉ ৈ۞ Viewী ੸ਊ ؼ ࣻ ੓׮. - ੤ ࢎਊࢿ੉ ਊ੉ೞ׮.

Slide 21

Slide 21 text

VIEW VIEW MODEL MODEL DataBinding Viewח ViewModelী bindableೞ׮. Notification Notification Update

Slide 22

Slide 22 text

Viewח ViewModelী bindableೞ׮. - View৬ ViewModelਸ ࢲ۽ োѾ ೞח Ѫ. - ࢎਊ੗੄ ೯زী ੄೧ ੑ۱ਸ ߉ওਸ ٸ (View -> ViewModel) - ࢎਊ੗੄ ೯زী ٮܲ View੄ ࢚కܳ ߸҃दெঠ ೡ ٸ (ViewModel -> View) - PS. ݽٚ ۽૒਷ bindingغח द੼ী Ѿ੿ػ׮.

Slide 23

Slide 23 text

Ӓېࢲ… VIEW VIEW MODEL MODEL DataBinding Databinding, LiveData RxJava Viewী ؀ೠ ੄ઓࢿ੉ ઁѢ غয
 ബҗ੸ਵ۽ ৉ೡҗ ଼੐ਸ աׂ ࣻ ੓׮ ViewModel ױةਵ۽ పझ౟о оמೞӝ ٸޙী పझ౟੄ ਊ੉ࢿ ژೠ ૐоػ׮. bindingغח द੼ী ݽٚ inputী ؀ೠ outputਸ ࢑୹ೞח ۽૒੉ ੿ ೧૑ӝ ٸޙী ѐߊ੗о ࢚కܳ ҙܻ೧ঠೞח ਤ೷ਸ ઴ৈળ׮. Databindingਸ ా೧ ݆ࣻ਷ ࠁੌ۞ ೒ۨ੉౟ ௏٘ܳ ઴ੌ ࣻ ੓׮.

Slide 24

Slide 24 text

How MVVM? Efficiently Testable

Slide 25

Slide 25 text

on Android? VIEW VIEW MODEL MODEL DataBinding Notification Update Notification

Slide 26

Slide 26 text

on Android! VIEW VIEW MODEL MODEL DataBinding LiveData UseCase Rx

Slide 27

Slide 27 text

VIEW VIEW MODEL MODEL DataBinding LiveData UseCase Rx XML (databindingUtil) Activity or Fragment or something on Android!

Slide 28

Slide 28 text

VIEW VIEW MODEL MODEL DataBinding LiveData UseCase Rx XML (databindingUtil) Activity or Fragment or something View੄ ࢚క߸ചܳ ઁ৻ೠ Router৉ೡ + ViewModelҗ Viewܳ binding ೞ ח ৉ೡਸ ࣻ೯ೠ׮. on Android!

Slide 29

Slide 29 text

୶о੸ਵ۽ ✨ - CleanArchitectureܳ ૑ೱ - Koinਸ ࢎਊೞৈ IOC(Inversion Of Control) ҳഅ - Spekਸ ࢎਊೞৈ ೯ز ઱ب Ѿҗ పझ౟ܳ ੘ࢿ.

Slide 30

Slide 30 text

য়ט ૓೯ೡ ৘ઁח…

Slide 31

Slide 31 text

https://github.com/omjoonkim/GitHubBrowserApp ௏٘ ੷੢ࣗ✨

Slide 32

Slide 32 text

package

Slide 33

Slide 33 text

package App Data Domain Remote

Slide 34

Slide 34 text

CleanArchitecture UI Data Domain Remote Presentation Koin!

Slide 35

Slide 35 text

Koin? - ઁয੄ ৉੹ਸ ҳഅ ೡ ࣻ ੓ѱ ب৬઱ח Library.(DI x) - Kotlinਵ۽ ҳഅغয ੓׮. - рಞೠ ࢎਊ ߑߨ! - AAC ژೠ ૑ਗ✨ - ઁয੄ ৉੹ਸ Service Locator ߑधਵ۽ ҳഅೠ׮. - Runtimeীࢲ ী۞ܳ ഛੋ ೡ ࣻ ੓׮.

Slide 36

Slide 36 text

Koin! val myModule: Module = module { viewModel { (id: String) -> MainViewModel(id, get(), get()) } viewModel { SearchViewModel(get()) } //app single { Logger() } single { AppSchedulerProvider() as SchedulersProvider } //domain single { GetUserData(get(), get()) } //data single { GithubBrowserDataSource(get()) as GitHubBrowserRepository } //remote single { GithubBrowserRemoteImpl(get(), get(), get()) as GithubBrowserRemote } single { RepoEntityMapper() } single { UserEntityMapper() } single { GithubBrowserServiceFactory.makeGithubBrowserService( BuildConfig.DEBUG, "https://api.github.com" ) } }

Slide 37

Slide 37 text

Koin! class App : Application() { override fun onCreate() { super.onCreate() startKoin( this, listOf(myModule) ) } }

Slide 38

Slide 38 text

SearchActivity

Slide 39

Slide 39 text

SearchView - Input? name clickSearchButton

Slide 40

Slide 40 text

SearchView - Output? STATE enableSearchButton Route goResultActivity

Slide 41

Slide 41 text

SearchViewModel - constructor class SearchViewModel( logger: Logger ) : BaseViewModel()

Slide 42

Slide 42 text

BaseViewModel abstract class BaseViewModel : ViewModel(){ protected val compositeDisposable : CompositeDisposable = CompositeDisposable() override fun onCleared() { super.onCleared() compositeDisposable.clear() } }

Slide 43

Slide 43 text

SearchViewModel - input, output, state interface SearchViewModelInPuts : Input { fun name(name: String) fun clickSearchButton() } interface SearchViewModelOutPuts : Output { fun state(): LiveData fun goResultActivity(): LiveData } data class SearchViewState( val enableSearchButton: Boolean )

Slide 44

Slide 44 text

SearchViewModel - properties private val name = PublishSubject.create() private val clickSearchButton = PublishSubject.create() val input = object : SearchViewModelInPuts { override fun name(name: String) = [email protected](name) override fun clickSearchButton() = [email protected](Parameter.CLICK) } private val state = MutableLiveData() private val goResultActivity = MutableLiveData() val output = object : SearchViewModelOutPuts { override fun state() = state override fun goResultActivity() = goResultActivity }

Slide 45

Slide 45 text

SearchViewModel - logic init { compositeDisposable.addAll( name.map { SearchViewState(it.isNotEmpty()) } .subscribe(state::setValue, logger::d), name.takeWhen(clickSearchButton) { _, t2 -> t2 } .subscribe(goResultActivity::setValue, logger::d) ) }

Slide 46

Slide 46 text

SearchViewModel - logic init { compositeDisposable.addAll( name.map { SearchViewState(it.isNotEmpty()) } .subscribe(state::setValue, logger::d), name.takeWhen(clickSearchButton) { _, t2 -> t2 } .subscribe(goResultActivity::setValue, logger::d) ) }

Slide 47

Slide 47 text

SearchView

Slide 48

Slide 48 text

SearchView - Databinding

Slide 49

Slide 49 text

SearchView - Databinding

Slide 50

Slide 50 text

SearchActivity class SearchActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView(this,…) binding.setLifecycleOwner(this) val viewModel = getViewModel() binding.viewModel = viewModel viewModel.output.goResultActivity() .observe { startActivity( Intent( Intent.ACTION_VIEW, Uri.parse("githubbrowser://repos/$it") ) ) } } }

Slide 51

Slide 51 text

SearchActivity - state binding class SearchActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView(this,…) binding.setLifecycleOwner(this) val viewModel = getViewModel() binding.viewModel = viewModel viewModel.output.goResultActivity() .observe { startActivity( Intent( Intent.ACTION_VIEW, Uri.parse("githubbrowser://repos/$it") ) ) } } }

Slide 52

Slide 52 text

SearchActivity - router binding class SearchActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView(this,…) binding.setLifecycleOwner(this) val viewModel = getViewModel() binding.viewModel = viewModel viewModel.output.goResultActivity() .observe { startActivity( Intent( Intent.ACTION_VIEW, Uri.parse("githubbrowser://repos/$it") ) ) } } }

Slide 53

Slide 53 text

ResultActivity

Slide 54

Slide 54 text

ResultView - Input? clickHomeButton clickUser

Slide 55

Slide 55 text

ResultView - Output? STATE title showLoading ROUTER refreshListData finish showErrorToast goProfileActivity

Slide 56

Slide 56 text

MainViewModel - constructor class MainViewModel( searchedUserName: String, private val getUserData: GetUserData, logger: Logger ) : BaseViewModel()

Slide 57

Slide 57 text

MainViewModel - input, output, state interface MainViewModelInputs : Input { fun clickUser(user: User) fun clickHomeButton() } interface MainViewModelOutPuts : Output { fun state(): LiveData fun refreshListData(): LiveData>> fun showErrorToast(): LiveData fun goProfileActivity(): LiveData fun finish(): LiveData } data class MainViewState( val showLoading: Boolean, val title: String )

Slide 58

Slide 58 text

MainViewModel - properties private val clickUser = PublishSubject.create() private val clickHomeButton = PublishSubject.create() val input: MainViewModelInputs = object : MainViewModelInputs { override fun clickUser(user: User) = clickUser.onNext(user) override fun clickHomeButton() = clickHomeButton.onNext(Parameter.CLICK) } private val state = MutableLiveData() private val refreshListData = MutableLiveData>>() private val showErrorToast = MutableLiveData() private val goProfileActivity = MutableLiveData() private val finish = MutableLiveData() val output = object : MainViewModelOutPuts { override fun state() = state override fun refreshListData() = refreshListData override fun showErrorToast() = showErrorToast override fun goProfileActivity() = goProfileActivity override fun finish() = finish }

Slide 59

Slide 59 text

MainViewModel - logic init { val error = PublishSubject.create() val userName = Observable.just(searchedUserName).share() val requestListData = userName.flatMapMaybe { getUserData.get(it).neverError(error) }.share() compositeDisposable.addAll( Observables .combineLatest( Observable.merge( requestListData.map { false }, error.map { false } ).startWith(true), userName, ::MainViewState ).subscribe(state::setValue, logger::d), requestListData.subscribe(refreshListData::setValue, logger::d), error.map { if (it is Error) it.errorText else UnExpected.errorText }.subscribe(showErrorToast::setValue, logger::d), clickUser.map { it.name }.subscribe(goProfileActivity::setValue, logger::d), clickHomeButton.subscribe(finish::call, logger::d) )

Slide 60

Slide 60 text

MainViewModel - logic val error = PublishSubject.create() val userName = Observable.just(searchedUserName).share() val requestListData = userName.flatMapMaybe { getUserData.get(it).neverError(error) }.share()

Slide 61

Slide 61 text

MainViewModel - logic compositeDisposable.addAll( Observables .combineLatest( Observable.merge( requestListData.map { false }, error.map { false } ).startWith(true), userName, ::MainViewState ).subscribe(state::setValue, logger::d), requestListData.subscribe(refreshListData::setValue, logger::d), error.map { if (it is Error) it.errorText else UnExpected.errorText }.subscribe(showErrorToast::setValue, logger::d), clickUser.map { it.name }.subscribe(goProfileActivity::setValue, logger::d), clickHomeButton.subscribe(finish::call, logger::d) )

Slide 62

Slide 62 text

MainViewModel - logic compositeDisposable.addAll( Observables .combineLatest( Observable.merge( requestListData.map { false }, error.map { false } ).startWith(true), userName, ::MainViewState ).subscribe(state::setValue, logger::d), requestListData.subscribe(refreshListData::setValue, logger::d), error.map { if (it is Error) it.errorText else UnExpected.errorText }.subscribe(showErrorToast::setValue, logger::d), clickUser.map { it.name }.subscribe(goProfileActivity::setValue, logger::d), clickHomeButton.subscribe(finish::call, logger::d) )

Slide 63

Slide 63 text

MainViewModel - logic compositeDisposable.addAll( Observables .combineLatest( Observable.merge( requestListData.map { false }, error.map { false } ).startWith(true), userName, ::MainViewState ).subscribe(state::setValue, logger::d), requestListData.subscribe(refreshListData::setValue, logger::d), error.map { if (it is Error) it.errorText else UnExpected.errorText }.subscribe(showErrorToast::setValue, logger::d), clickUser.map { it.name }.subscribe(goProfileActivity::setValue, logger::d), clickHomeButton.subscribe(finish::call, logger::d) )

Slide 64

Slide 64 text

MainViewModel - logic compositeDisposable.addAll( Observables .combineLatest( Observable.merge( requestListData.map { false }, error.map { false } ).startWith(true), userName, ::MainViewState ).subscribe(state::setValue, logger::d), requestListData.subscribe(refreshListData::setValue, logger::d), error.map { if (it is Error) it.errorText else UnExpected.errorText }.subscribe(showErrorToast::setValue, logger::d), clickUser.map { it.name }.subscribe(goProfileActivity::setValue, logger::d), clickHomeButton.subscribe(finish::call, logger::d) )

Slide 65

Slide 65 text

MainViewModel - logic compositeDisposable.addAll( Observables .combineLatest( Observable.merge( requestListData.map { false }, error.map { false } ).startWith(true), userName, ::MainViewState ).subscribe(state::setValue, logger::d), requestListData.subscribe(refreshListData::setValue, logger::d), error.map { if (it is Error) it.errorText else UnExpected.errorText }.subscribe(showErrorToast::setValue, logger::d), clickUser.map { it.name }.subscribe(goProfileActivity::setValue, logger::d), clickHomeButton.subscribe(finish::call, logger::d) )

Slide 66

Slide 66 text

MainViewModel - logic compositeDisposable.addAll( Observables .combineLatest( Observable.merge( requestListData.map { false }, error.map { false } ).startWith(true), userName, ::MainViewState ).subscribe(state::setValue, logger::d), requestListData.subscribe(refreshListData::setValue, logger::d), error.map { if (it is Error) it.errorText else UnExpected.errorText }.subscribe(showErrorToast::setValue, logger::d), clickUser.map { it.name }.subscribe(goProfileActivity::setValue, logger::d), clickHomeButton.subscribe(finish::call, logger::d) )

Slide 67

Slide 67 text

MainView

Slide 68

Slide 68 text

MainView - Databinding

Slide 69

Slide 69 text

MainView - Databinding

Slide 70

Slide 70 text

MainActivity val binding = DataBindingUtil.setConten….. binding.setLifecycleOwner(this) val viewModel = getViewModel { parametersOf(intent.data.path.substring(1)) } binding.viewModel = viewModel actionbarInit(binding.toolbar, onClickHomeButton = { viewModel.input.clickHomeButton() })

Slide 71

Slide 71 text

MainActivity with(viewModel.output) { refreshListData().observe { (user, repos) -> binding.recyclerView.adapter = MainListAdapter( user, repos, viewModel.input::clickUser ) } showErrorToast().observe { showToast(it) } goProfileActivity().observe { startActivity( Intent( Intent.ACTION_VIEW, Uri.parse("githubbrowser://repos/$it") ) ) } finish().observe { onBackPressed() } }

Slide 72

Slide 72 text

Test

Slide 73

Slide 73 text

HOW? - ࢎ੹ળ࠺ - Testܳ ਤೠ SchedulersProviderܳ ٜ݅যঠ ೠ׮. - Testܳ ਤೠ DummyApiServiceܳ ٜ݅যঠ ೠ׮. - Spek + LiveDataܳ э੉ పझ౟ ೞӝ ਤೠ ௏٘ ੘ࢿ. - Spekਸ ੉ਊೠ పझ౟ ௏٘ ੘ࢿ - Feature - Scenario - Given - When - Then

Slide 74

Slide 74 text

TestSchedulerProvider class TestSchedulerProvider : SchedulersProvider { override fun io() = Schedulers.trampoline() override fun ui() = Schedulers.trampoline() }

Slide 75

Slide 75 text

TestDummyGithubBrowserService class TestDummyGithubBrowserService : GithubBrowserService { override fun getUserInfo(userName: String): Single = Single.just( UserModel("omjoonkim", "") ) override fun getUserRepos(userName: String): Single> = Single.just( listOf( RepoModel("repo1", "repo1 description", "1"), RepoModel("repo2", "repo2 description", "2"), RepoModel("repo3", "repo3 description", "3") ) ) }

Slide 76

Slide 76 text

DI for Test val testModule = module { single(override = true) { TestSchedulerProvider() as SchedulersProvider } single(override = true) { TestDummyGithubBrowserService() as GithubBrowserService } } val test_module = listOf(myModule, testModule)

Slide 77

Slide 77 text

for Spek + LiveData beforeEachTest { ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() { override fun executeOnDiskIO(runnable: Runnable) { runnable.run() } override fun isMainThread(): Boolean { return true } override fun postToMainThread(runnable: Runnable) { runnable.run() } }) } afterEachTest { ArchTaskExecutor.getInstance().setDelegate(null) }

Slide 78

Slide 78 text

MainViewModelSpec object MainViewModelSpec : KoinSpek({ beforeEachTest {…} afterEachTest {…} lateinit var userName: String val viewModel: MainViewModel by inject { parametersOf(userName) } val getUserData: GetUserData by inject() Feature("MainViewModel spec") {…} })

Slide 79

Slide 79 text

Scenario1 Scenario("ਬ੷о ചݶী ٜযয়ݶ Ѩ࢝ೠ ਬ੷੄ ೐۽೙,੷੢ࣗ ؘ੉ఠо ੿࢚੸ਵ۽ ࠁৈঠ ೠ׮") { Given("Ѩ࢝ೞ۰ח ਬ੷੄ ੉ܴ਷ omjoonkim੉׮"){ userName = "omjoonkim" } Then("ചݶী Ѩ࢝ೠ ਬ੷੄ ؘ੉ఠо ੿࢚੸ਵ۽ աఋդ׮") { assertEquals( getUserData.get(userName).blockingGet(), viewModel.output.refreshListData().value ) } }

Slide 80

Slide 80 text

Scenario2 Scenario("ਬ੷ ೐۽೙ਸ ௿ܼೞݶ ਬ੷੄ ೐۽೙ ചݶਵ۽ ੉زغযঠ ೠ׮") { When("೐۽೙ਸ ௿ܼ ೮ਸ ٸ") { viewModel.input.clickUser( viewModel.output.refreshListData().value?.first ?: throw IllegalStateException() ) } Then("೧׼ ਬ੷੄ ೐۽೙ ചݶਵ۽ ੉ز ػ׮") { assertEquals( viewModel.output.refreshListData().value?.first?.name ?: throw IllegalStateException(), viewModel.output.goProfileActivity().value ) } }

Slide 81

Slide 81 text

Scenario3 Scenario("കߡౡਸ ௿ܼೞݶ ചݶ੉ ੿࢚੸ਵ۽ ઙܐغযঠ ೠ׮.") { When("കߡౡਸ ௿ܼ ೮ਸ ٸ") { viewModel.input.clickHomeButton() } Then("ചݶ੉ ੿࢚੸ਵ۽ ઙܐ ػ׮.") { assertEquals( Unit, viewModel.output.finish().value ) } }

Slide 82

Slide 82 text

✨✨✨

Slide 83

Slide 83 text

More……. + TMI

Slide 84

Slide 84 text

Dagger2 vs Koin - Heavy vs light - Dependency Injection vs ServiceLocator - CompileTime vs RunTime https://twitter.com/jakewharton/status/908419644742098944

Slide 85

Slide 85 text

Spekҗ Koin੄ ഐജࢿ - Spek + Koinਸ ࢎਊೞ۰ݶ ୶о੸ਵ۽ ੘স೧ঠ ೞח ௏ٜ٘੉ ੓׮. - Spek੄ ҳز ߑधҗ Koinਸ ࢎਊೞח ߑध੉ ࢲ۽ ୽ج غӝ ٸޙ https://github.com/InsertKoinIO/koin/pull/107

Slide 86

Slide 86 text

ѐࢶ੄ ৈ૑ + ইए਍ ੼ - Datbinding੉ kotlinী 100% ഐജغ૑ ঋח׮. - ۈ׮ ೣࣻܳ xmlীࢲ чਵ۽ ૑੿೧઴ ࣻ হ׮. - Router - Presentation module ܻ࠙

Slide 87

Slide 87 text

Thank you✨✨ by @omjoonkim