In this project, I have used clean architecture with a MVI pattern.
control-flow
jetpack compose
kotlin coroutine
coroutine flow
retrofit
okhttp
view-model
hilt
timber
:core:data
It includes classes that are used to communicate with the server.
:core:domain
It includes use case classes between data module and presentation module.
:core:framework
It includes the classes used in the presentation module.
:presentation
It includes the ui to interact with the user and also contains the viewModel class.
In this project, the Amadeus website services facilitate obtaining a list of hotels, involving a series of three essential tasks:
Note: Please utilize your unique API_KEY
and API_SECRET
, as mine are concealed. Once obtained, insert them into the RemoteConfig Class
API_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
API_SECRET = "xxxxxxxxxxxxxxxx"
- Invoking the
authorization
web service to procure the crucialaccess_token
- Safeguarding the obtained
access_token
by storing it through theSharedPreferences
functionality. - Retrieving the catalog of hotels by utilizing the
hotelsListByCity
web service.
To orchestrate these asynchronous tasks effectively, the Control-Flow library assumes the responsibility of task management. In the provided code samples:
- The
AuthorizationTask
class takes charge of the initial task. - Subsequently, the
SaveAccessTokenTask
class handles the second task. - Finally, the
GetHotelsListTask
class executes the third task.
val engine = ControlFlow(object: WorkFlowTracker {
override fun started(controlFlow: ControlFlow) {
Timber.tag("TAG").e("flow Started")
setState(state = ViewState.Loading)
}
override fun taskStatus(controlFlow: ControlFlow, taskFlow: TaskFlow, state: State) {
when(state) {
State.Started -> {
when(taskFlow.isRollback) {
true -> {
Timber.tag("TAG").e("rollback task Started, name is: ${taskFlow.taskName}")
}
else -> {
Timber.tag("TAG").e("task Started, name is: ${taskFlow.taskName}")
}
}
}
State.InProgress -> {
when(taskFlow.isRollback) {
true -> {
Timber.tag("TAG").e("rollback task InProgress, name is: ${taskFlow.taskName}")
}
else -> {
Timber.tag("TAG").e("task InProgress, name is: ${taskFlow.taskName}")
}
}
}
}
}
override fun completed(controlFlow: ControlFlow) {
Timber.tag("TAG").e("flow completed" )
}
}).apply {
startWith(first = AuthorizationTask(useCase = useCase))
then(next = SaveAccessTokenTask(cashManager = sharedPrefCashManager))
then(next = GetHotelsListTask(useCase = getHotelsUseCase))
}
Note: To activate the Retry mechanism, I've configured the properties of the AuthorizationTask
class.
In case of a TimeoutException
error, the task will be retried thrice, with a one-second interval between each attempt.
class AuthorizationTask(
private val useCase: AuthorizationUseCase
) : Dispatcher(), TaskProcessor {
override val info: TaskInfo
get() = TaskInfo().apply {
index = 0
name = AuthorizationTask::class.java.name
retry = RetryStrategy().apply {
count = 3
causes = setOf(TimeoutException::class)
delay = 1000L
}
runIn = Dispatchers.IO
}
override suspend fun doProcess(param: Any?): Flow<TaskStatus> {
...
}
}
Note: the output of the first task undergoes conversion via the transformer
method before being passed to the second task.
class AuthorizationTask(
private val useCase: AuthorizationUseCase
) : Dispatcher(), TaskProcessor {
override val info: TaskInfo
get() = ...
override suspend fun doProcess(param: Any?): Flow<TaskStatus> {
return launchFlow(action = { useCase(params = BaseUseCase.None()) },
transformer = { TransformData(data = it.body().toJson().fromJson<ResAuthorization>()?.accessToken) },
actionCondition = {
when(it.isSuccessful) {
true -> {
ConditionData(status = Boolean.successMode())
}
else -> {
val jObjError: JSONObject? =
it.errorBody()?.string()
?.let { it1 -> JSONObject(it1) }
var errorDescription = ""
var errorCode: Int = -1
if(jObjError?.has("error_description") == true && jObjError.has("code")) {
errorDescription = jObjError.getString("error_description")
errorCode = jObjError.getInt("code")
}
ConditionData(status = Boolean.failureMode(), throwable = HttpException(code = errorCode, message = errorDescription))
}
}
})
}
}
Note: If an error occurs while calling the hotelsListByCity
web service, the doRollbackProcess
method run automatically. It effectively manages the removal of the previously acquired access-token
, playing a crucial role in evaluating the rollback function.
class SaveAccessTokenTask(
private val cashManager: SharedPrefCashManager
): Dispatcher(), RollbackTaskProcessor {
override val info: TaskInfo
get() = TaskInfo().apply {
index = 1
name = SaveAccessTokenTask::class.java.name
runIn = Dispatchers.IO
}
override val rollbackInfo: RollbackInfo
get() = RollbackInfo().apply {
index = 1
name = SaveAccessTokenTask::class.java.name
runIn = Dispatchers.IO
}
override suspend fun doProcess(param: Any?): Flow<TaskStatus> {
return launch { cashManager.putPreference(key = ACCESS_TOKEN, value = param) }
}
override suspend fun doRollbackProcess(): Flow<TaskStatus> {
return launch { cashManager.putPreference(key = ACCESS_TOKEN, value = String.empty()) }
}
}
To activate the rollback mechanism automatically after any task encounters an error, ensure to set the runAutomaticallyRollback
flag to true
within the start
method. By default, the value of this flag is runAutomaticallyRollback= false
// Create a ControlFlow instance
val controlFlow = ControlFlow(object : WorkFlowTracker {
// Implement work Flow callback methods
})
// Define your tasks
controlFlow.startWith(MyTask())
controlFlow.then(AnotherTask())
controlFlow.then(AnotherTask())
// Set up TaskStatusTracker if needed
controlFlow.useTaskStatusTracker(object : TaskStatusTracker {
// Implement callback methods
})
// Set up RollbackStatusTracker if needed
controlFlow.useRollbackStatusTracker(object : RollbackStatusTracker {
// Implement callback methods
})
// Start executing tasks
controlFlow.start(runAutomaticallyRollback= true)
If you have a suggestion that would make this better, please fork the repo and create a pull request. Remember to show your support by giving the project a star. Thank you once more :)
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/YourFeatureName
) - Commit your Changes (
git commit -m 'Add some YourFeatureName'
) - Push to the Branch (
git push origin feature/YourFeatureName
) - Open a Pull Request
You can contact me via email soltaniyan.mohsen@gmail.com
I appreciate your interest.
MIT License
Copyright (c) 2023 Related Code
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.