Application for putting some gamification into diet. Keep diet, do workout, get cheat day as reward. Track your weight progress. Traditional views with fragments, jetpack, MVI based on MVVM, backend written with ktor. Done with intention of presenting coding skills, my approach for app architecture, testing and having some playground to test new frameworks and patterns.
Source code of backend for project: https://github.com/mariuszmarzec/fiteo/blob/master/src/jvmMain/kotlin/com/marzec/cheatday/CheatDay.kt
- Setup above mentioned backend locally or on some services like AWS Elastic Beanstalk
- Before you will be able to run cheat day, declare variables in local.properties like below to configure connection with backend and release key store
- Use localDebug or localRelease build variant to check it without api
storeFile=~/key_releases/cheat_day_release.jks
keyAlias=<KEY_ALIAS>
keyPassword=<KEY_PASSWORD>
storePassword=<STORE_PASSWORD>
prod.apiUrl=http://127.0.0.1
prod.authHeader=Authorization
test.apiUrl=http://127.0.0.1/test
test.authHeader=Authorization-Test
- Sync project and enjoy
Architecture is built upon my own implementation of MVI. More about in my other repository -
QuickMVI. Whole concept is based on Store
class wrapped
into android view model. To fire any action everything you need is declaring intent method which
trigger actions. Reducer and sideEffect are tied closely with fired action.
fun onLoginButtonClicked() = intent<Content<User>> {
onTrigger(isCancellableFlowTrigger = true) {
state.ifDataAvailable {
loginRepository.login(login, password)
.cancelFlowsIf { it is Content.Data }
}
}
reducer {
state.reduceContentNoChanges(resultNonNull())
}
emitSideEffect {
(resultNonNull() as? Content.Data)?.let { LoginSideEffects.OnLoginSuccessful }
}
}
Builder method intent
lets to define onTrigger as action could be called. Reducer is a method which
handle state changes in response of changing onTrigger's result. Every change of state could emit
side effect. In case of android implementation, side effect are exposed as sideEffects
flow from
ViewModel
. Stores contains state which is needed to render view, but keep data which could needed
also during communication with business layer.
State
is a helper class to deal with three basic states: Loading
, Data
and Error
. Together
with some extensions and utils method helps to reduce repeating code, simplifying code and
unifying UI behaviour
Renderers
are classes responsible for manipulating UI in response of changing state. They could
map state's model into UI state model and modify views directly. They are introduce in the code to be
responsible for part of UI rather than whole screen, but it depends on how complicated is view logic
and UI's complexity.
Business logic, store logic and repositories are tested with unit tests. Approach used for writing
is relying on given, when, then implicit separation. Every test starts with preparing appropriate
mocks, then action is made and result is saved in variable, which will be asserted with Truth
library.
@Test
fun onLoginButtonClicked() = test {
val viewModel = viewModel(defaultState)
val states = viewModel.test(this)
coEvery {
loginRepository.login(
email = "login",
password = "password"
)
} returns flowOf(Content.Loading(), Content.Data(user))
viewModel.onLoginButtonClicked()
states.isEqualTo(
defaultState,
State.Loading(defaultData),
LoginSideEffects.OnLoginSuccessful
)
}
Testing UI layer together with renderers and any mappers is built upon screenshot testing. Before i used Shot, but this library is built upon not maintained facebook screenshot test framework and doesn't work for API 29+. So i decided to migrate to paparazzi. Screenshot to compare are saved in app/test/snapshots.
Always before running tests it is needed to fire commands for API >= 29:
adb shell settings put global hidden_api_policy_pre_p_apps 1
adb shell settings put global hidden_api_policy_p_apps 1
adb shell settings put global hidden_api_policy 1
To run recording new screenshot:
gradlew recordPaparazziStageDebug
To verify UI with recorded earlier screenshots:
gradlew verifyPaparazziStageDebug
Testing scenarios contained in TESTING_SCENARIOS.md are translated into Kaspresso based tests. They used MockWebServer to mimic server responses. Test orchestrator is required to clean user data before every test.
Other automated tests in project are tests of room's DAOs classes and migration of database between versions.
Code coverage in project was made with JaCoCo Plugin.
To run generate code coverage test report:
gradlew jacocoTestReport
Data are store in application in shared preferences with Jetpack Data Store and in SQL Lite database using Room. Loading data from API is done with first cache pattern. It means data are loaded always from data base. Fetched data from API when request succeed into database. Thanks to Room and kotlin flow, on every update in database, new values are emitted to be presented on screen.