Skip to content

Commit a33f168

Browse files
fragarsiefragarsie
authored andcommitted
Update sample app
1 parent 8a727ab commit a33f168

57 files changed

Lines changed: 936 additions & 481 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,11 @@ data class LoginAction(val username: String,
1818
The dispatcher receives Actions and broadcast payloads to registered callbacks. The instance of the Dispatcher must be unique across the whole application and it will execute all the logic in the main thread making state muations synchronous.
1919

2020
```kotlin
21-
//Dispatch the action in the current Thread
21+
//Dispatch an action on the main thread synchronously
2222
dispatcher.dispatch(LoginAction(username = "user", password = "123"))
2323

24-
//Dispatch the action in the UI Thread, this action will be processed in the next event loop
25-
dispatcher.dispatchOnUi(LoginAction(username = "user", password = "123"))
26-
27-
//Post and event that will dispatch the action on the Ui thread
28-
//and block until the dispatch is complete. Only useful when calling from a background thread.
29-
dispatcher.dispatchOnUiSync(LoginAction(username = "user", password = "123"))
24+
//Post an event that will dispatch the action on the UI thread and return immediately.
25+
dispatcher.dispatchAsync(LoginAction(username = "user", password = "123"))
3026
```
3127

3228
### Store
@@ -55,7 +51,15 @@ class SessionStore : Store<SessionState>() {
5551
```
5652

5753
### Generated code
58-
MiniActionReducer explanation. //TODO
54+
Mini uses an annotation processor to generate code automatically based on the `@Reducer` annotations in your code.
55+
Annotating a function inside a `Store` with `@Reducer` will generate all the code needed to call the right methods of your stores when an `Action` goes though the `Dispatcher`.
56+
57+
A method annotated with `@Reducer` must:
58+
- Receive a class that inherits from `Action` by parameter
59+
- Return an State of the same type that the store which contains the function
60+
- Optionally, it can receive also another `State` as second parameter. This is useful for unitary testing purposes.
61+
62+
All the generated code is contained inside the class `MiniActionReducer`.
5963

6064
### View changes
6165
Each ``Store`` exposes a custom `StoreCallback` though the method `observe` or a `Flowable` if you wanna make use of RxJava. Both of them emits changes produced on their states, allowing the view to listen reactive the state changes. Being able to update the UI according to the new `Store` state.
@@ -79,13 +83,11 @@ A Task is a basic object to represent an ongoing process. They should be used in
7983
Having the next code:
8084

8185
```kotlin
82-
8386
data class LoginAction(val username: String, val password: String)
84-
data class LoginCompleteAction(val loginTask: Task,
85-
val user: User?)
86-
87-
data class SessionState(val loginTask: Task = taskIdle(),
88-
val loggedUser: User? = null)
87+
data class LoginCompleteAction(val loginTask: Task, val user: User?)
88+
```
89+
```kotlin
90+
data class SessionState(val loginTask: Task = taskIdle(), val loggedUser: User? = null)
8991

9092
class SessionStore @Inject constructor(val controller: SessionController) : Store<SessionState>() {
9193
@Reducer
@@ -185,9 +187,9 @@ fun login_redirects_to_home_with_success_task() {
185187
### Import the library
186188
To setup Mini in your application, first you will need to add the library itself together with the annotation processor:
187189
```groovy
188-
implementation 'com.github.pabloogc:Mini:1.0.5'
189-
kapt 'com.github.pabloogc.Mini:mini-processor:1.0.5'
190-
androidTestImplementation 'com.github.pabloogc.Mini:mini-android-testing:1.0.5' //Optional
190+
implementation 'com.github.pabloogc:Mini:1.1.0'
191+
kapt 'com.github.pabloogc.Mini:mini-processor:1.1.0'
192+
androidTestImplementation 'com.github.pabloogc.Mini:mini-android-testing:1.1.0' //Optional
191193
```
192194

193195
### Setting up your App file

app/build.gradle

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ buildscript {
77
rx_android_version = "2.0.2"
88
leak_canary_version = "1.5"
99
butterkinfe_version = "8.4.0"
10-
support_version = "26.0.1"
10+
support_version = "27.0.0"
1111
target_sdk_version = 26
1212
}
1313
}
@@ -34,7 +34,7 @@ kotlin {
3434
//Android
3535
android {
3636
compileSdkVersion target_sdk_version
37-
buildToolsVersion '27.0.3'
37+
buildToolsVersion '28.0.3'
3838

3939
defaultConfig {
4040
applicationId "com.minivac.mini"
@@ -78,7 +78,7 @@ dependencies {
7878
implementation project(":mini-common")
7979
kapt project(":mini-processor")
8080

81-
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
81+
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
8282

8383
//Support
8484
implementation "com.android.support:appcompat-v7:$support_version"
@@ -93,9 +93,11 @@ dependencies {
9393
kapt "com.google.dagger:dagger-compiler:$dagger_version"
9494
kapt "com.google.dagger:dagger-android-processor:$dagger_version"
9595

96-
//Rx
97-
implementation "io.reactivex.rxjava2:rxjava:$rx_version"
98-
implementation "io.reactivex.rxjava2:rxandroid:$rx_android_version"
96+
testImplementation 'junit:junit:4.12'
97+
androidTestImplementation project(":mini-android-testing")
98+
androidTestImplementation 'com.android.support.test:runner:1.0.2'
99+
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
100+
androidTestImplementation 'com.agoda.kakao:kakao:1.4.0'
99101
}
100102

101103

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package org.sample
2+
3+
import com.agoda.kakao.KEditText
4+
import com.agoda.kakao.KProgressBar
5+
import com.agoda.kakao.KTextView
6+
import com.agoda.kakao.Screen
7+
import mini.onUiSync
8+
import mini.taskRunning
9+
import org.junit.Assert
10+
import org.junit.Rule
11+
import org.junit.Test
12+
import org.sample.session.store.LoginWithCredentialsAction
13+
import org.sample.session.store.SessionState
14+
import org.sample.session.store.SessionStore
15+
16+
class LoginScreen : Screen<LoginScreen>() {
17+
val email: KEditText = KEditText { withId(R.id.emailInput) }
18+
val password: KEditText = KEditText { withId(R.id.passwordInput) }
19+
val credentialsLoginButton: KTextView = KTextView { withId(R.id.loginCredentialsButton) }
20+
val progress: KProgressBar = KProgressBar { withId(R.id.progress) }
21+
}
22+
23+
class LoginActivityTest {
24+
25+
@get:Rule
26+
val activity = testActivity(LoginActivity::class)
27+
@get:Rule
28+
val cleanState = cleanStateRule()
29+
@get:Rule
30+
val testDispatcher = testDispatcherRule()
31+
32+
@Test
33+
fun login_with_credentials_dispatch_right_action() {
34+
val screen = LoginScreen()
35+
val anyEmail = "notADreadLord@gmail.com"
36+
val anyPassword = "GarroshDidNothingWrong"
37+
38+
screen {
39+
email {
40+
scrollTo()
41+
typeText(anyEmail)
42+
}
43+
closeSoftKeyboard()
44+
45+
password {
46+
scrollTo()
47+
typeText(anyPassword)
48+
}
49+
closeSoftKeyboard()
50+
51+
credentialsLoginButton {
52+
scrollTo()
53+
click()
54+
}
55+
56+
//Check action
57+
view.check { _, _ ->
58+
val expectedAction = LoginWithCredentialsAction(anyEmail, anyPassword)
59+
Assert.assertTrue(testDispatcher.testInterceptor.actions.any { it == expectedAction })
60+
}
61+
}
62+
}
63+
64+
@Test
65+
fun progress_bar_is_show_when_logging() {
66+
val screen = LoginScreen()
67+
val sessionStore = store<SessionStore>()
68+
69+
screen { progress.isGone() }
70+
71+
//Set login state to success
72+
onUiSync {
73+
val state = SessionState().copy(loginTask = taskRunning())
74+
sessionStore.setTestState(state)
75+
}
76+
77+
screen { progress.isVisible() }
78+
}
79+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.sample
2+
3+
import android.app.Activity
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.support.test.InstrumentationRegistry
7+
import android.support.test.rule.ActivityTestRule
8+
import com.durmin.mini_android_testing.CleanStateRule
9+
import com.durmin.mini_android_testing.TestDispatcherRule
10+
import mini.Store
11+
import org.sample.core.app
12+
import kotlin.reflect.KClass
13+
14+
fun cleanStateRule() = CleanStateRule(app.component.stores().values.toList())
15+
fun testDispatcherRule() = TestDispatcherRule(app.component.dispatcher())
16+
17+
inline fun <reified T : Store<*>> store(): T {
18+
return app.component.stores()[T::class.java] as T
19+
}
20+
21+
fun <T : Activity> testActivity(clazz: KClass<T>,
22+
createIntent: ((context: Context) -> Intent)? = null): ActivityTestRule<T> {
23+
24+
//Overriding the rule throws an exception
25+
if (createIntent == null) return ActivityTestRule<T>(clazz.java)
26+
return object : ActivityTestRule<T>(clazz.java) {
27+
override fun getActivityIntent(): Intent = createIntent(InstrumentationRegistry.getTargetContext())
28+
}
29+
}
30+
31+
inline fun <reified T : Activity> testActivity(intent: Intent): ActivityTestRule<T> =
32+
ActivityTestRule(T::class.java)

app/src/main/AndroidManifest.xml

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
1-
<?xml version="1.0" encoding="utf-8"?>
21
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3-
package="com.minivac.mini">
2+
xmlns:tools="http://schemas.android.com/tools" package="org.sample">
3+
4+
<application android:allowBackup="true"
5+
android:label="@string/app_name"
6+
android:icon="@mipmap/ic_launcher"
7+
android:roundIcon="@mipmap/ic_launcher_round"
8+
android:supportsRtl="true"
9+
android:name=".core.App"
10+
android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning">
11+
12+
<activity
13+
android:name=".LoginActivity"
14+
android:screenOrientation="portrait">
415

5-
<application
6-
android:allowBackup="true"
7-
android:icon="@mipmap/ic_launcher"
8-
android:label="@string/app_name"
9-
android:name="org.sample.todo.core.App"
10-
android:supportsRtl="true"
11-
android:theme="@style/AppTheme">
12-
<activity android:name="org.sample.todo.MainActivity">
1316
<intent-filter>
1417
<action android:name="android.intent.action.MAIN"/>
1518

1619
<category android:name="android.intent.category.LAUNCHER"/>
1720
</intent-filter>
1821
</activity>
1922

20-
<activity android:name="org.sample.todo.SecondActivity"/>
23+
<activity
24+
android:name=".HomeActivity"
25+
android:screenOrientation="portrait"/>
2126
</application>
22-
23-
</manifest>
27+
</manifest>

app/src/main/ic_launcher-web.png

14.9 KB
Loading
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package org.sample
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import android.os.Bundle
6+
import kotlinx.android.synthetic.main.home_activity.*
7+
import mini.Dispatcher
8+
import mini.mapNotNull
9+
import org.sample.core.dagger.BaseActivity
10+
import org.sample.session.store.SessionStore
11+
import org.sample.session.store.SignOutAction
12+
import javax.inject.Inject
13+
14+
class HomeActivity : BaseActivity() {
15+
16+
@Inject
17+
lateinit var dispatcher: Dispatcher
18+
19+
@Inject
20+
lateinit var sessionStore: SessionStore
21+
22+
companion object {
23+
fun newIntent(context: Context): Intent = Intent(context, HomeActivity::class.java)
24+
}
25+
26+
override fun onCreate(savedInstanceState: Bundle?) {
27+
super.onCreate(savedInstanceState)
28+
setContentView(R.layout.home_activity)
29+
initializeInterface()
30+
startListeningSessionChanges()
31+
}
32+
33+
private fun startListeningSessionChanges() {
34+
sessionStore.flowable()
35+
.mapNotNull { it.loggedUser }
36+
.subscribe { updateEmail(it.email) }
37+
.track()
38+
39+
sessionStore.flowable()
40+
.map { it.logged }
41+
.filter { !it }
42+
.subscribe { goToLogin() }
43+
.track()
44+
}
45+
46+
47+
private fun initializeInterface() {
48+
signOut.setOnClickListener { signOut() }
49+
}
50+
51+
private fun updateEmail(value: String) {
52+
email.text = value
53+
}
54+
55+
private fun signOut() {
56+
dispatcher.dispatch(SignOutAction())
57+
}
58+
59+
private fun goToLogin() {
60+
val intent = LoginActivity.newIntent(this).apply {
61+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
62+
}
63+
startActivity(intent)
64+
}
65+
}

0 commit comments

Comments
 (0)