use_in_case 1.3.0
use_in_case: ^1.3.0 copied to clipboard
An extendable library that provides functionality for use cases
Use-In-Case (UIC) Interactor #
This library declares a base interactor interface aswell as a corresponding progress-interactor class. In order to use them there are quiet a lot of modifiers that can be used to do actions inside the invocation-flow of an interactor.
Interactor Types #
| Type name | Parameterized | Resulting |
|---|---|---|
| ParameterizedResultInteractor | Yes | Yes |
| ParameterizedInteractor | Yes | No |
| ResultInteractor | No | Yes |
| Interactor | No | No |
Usage #
How to call an interactor in your code:
// Define an interactor that does something. He must extend/implement a type mentioned above.
final class StringToIntConverter implements ParameterizedResultInteractor<String, int> {
@override
Future<int> runUnsafe(String input) async {
return int.parse(input);
}
}
/// ...
// Create an instance of the interactor
final converter = StringToIntConverter();
/// ...
Future<int> _ = converter.getOrThrow("123"); // Outputs: 123
Future<int?> _ = converter.getOrNull("not-a-number"); // Outputs: null
Future<int> _ = converter.getOrElse("word", (_) => -1); // Outputs: -1
Future<void> _ = converter.run("123"); // Outputs: Nothing (void)
Future<void> _ = converter.run("word"); // Doesn't throw & returns void
Future<void> _ = converter.runUnsafe("123"); // Outputs: Nothing (void)
Future<void> _ = converter.runUnsafe("word"); // Throws exception
| Method name | Description |
|---|---|
getOrThrow |
Calls the interactor and throws an exception if the interactor fails. |
getOrNull |
Calls the interactor and returns null if the interactor fails. |
getOrElse |
Calls the interactor and returns a fallback value if the interactor fails. |
run |
Calls the interactor and ignores the result. Also this method does not throw. |
runUnsafe |
Calls the interactor and throws an exception in case of a failure. The return type is void. |
Customization #
The core feature of uic-interactor is the ability to customize the invocation-flow of an interactor. This can be achieved by chaining multiple decorators to the interactor.
In the end your invocation-flow might look like this:
final result = stringToIntConverter
.timeout(const Duration(seconds: 5))
.before((input) => print("Trying to convert $input to string."))
.after((output) => print("Successfully converted number to string. Result: $output"))
.intercept((exception) => print("Failed to convert number to string. Exception caught: $exception"))
.getOrNull("123") // Call the interactor with a parameter
// ...
Right now there are couple of decorators available:
| Decorator name | Description | Workflow |
|---|---|---|
after |
Adds a hook that is called after the interactor is executed. | [after] |
before |
Adds a hook that is called before the interactor is executed. | [before] |
watchBusyState |
Adds a hook that is called when the interactor starts & ends. | [busystate] |
eventually |
Adds a hook that is called when the interactor finishes. | [finally] |
intercept |
Adds a hook that is called when the interactor fails. | [catch] |
typedIntercept |
Adds a hook that is called when the interactor fails with a specific exception type. | [catch] |
checkedIntercept |
Adds a hook that is called when the interactor fails and a given predicate returns true. | [catch] |
log |
Times the operation and produces a message that can be displayed through logging library. | [log] |
map |
Converts the output of the interactor. | [map] |
recover |
Calls a given callback when an exception has been thrown. The callback must return a fallback output. | [recover] |
typedRecover |
Calls a given callback when a specific exception has been thrown. The callback must return a fallback output. | [recover] |
checkedRecover |
Calls a given callback when the given predicate returns true. The callback must return a fallback output. | [recover] |
timeout |
Adds a timeout to the interactor. | [timeout] |
Order Matters #
The graphic below shows in which order each decorator is going to append itself around the execution.
| [workflow visualization] |
|
Declaring your own customizations #
It is possible to write custom decorators that modify that invocation-flow of the interactor.
Examples can be found here.
extension CustomModifier<Input, Output> on ParameterizedResultInteractor<Input, Output> {
ParameterizedResultInteractor<Input, Output> customModifier() {
return InlinedParameterizedResultInteractor((input) {
print("I am here!")
return await runUnsafe(input);
});
}
}
Progress Interactors #
In some cases the interactor might need to publish progress information.
Given a FileDownloadInteractor that downloads a file from the internet, it might look like this:
typedef SourceUrl = String;
typedef DestinationFilepath = String;
typedef Parameter = ({
SourceUrl sourceUrl,
DestinationFilepath destinationFilepath
});
typedef DownloadedBytes = int;
typedef DownloadProgress = int;
final class FileDownloadInteractor extends ParameterizedResultProgressInteractor<
Parameter, DownloadedBytes, DownloadProgress> {
@override
Future<DownloadedBytes> runUnsafe(Parameter input) async {
// TODO: Implement your file download here
await emitProgress(0);
// Download ...
await emitProgress(100);
}
}
// ...
void main() {
final downloadService = FileDownloadInteractor();
final result = await downloadService
.receiveProgress((progress) async {
print('Download-Progress: $progress%');
})
.getOrThrow((
sourceUrl: 'https://example.com/image.jpg',
destinationFilepath: 'image.jpg'
));
print(result);
}
Just like the default interactor types written above, the ProgressInteractor provides a single method called onProgress which must be called before all other decorators. It gets called whenever the interactor wants to publish a progress-value to the caller. Due to API limitations it can only be registerd once in the method-pipe.
The naming-convention mirrors the previously declared interactors from above.
| Type name | Parameterized | Resulting |
|---|---|---|
| ParameterizedResultProgressInteractor | Yes | Yes |
| ParameterizedProgressInteractor | Yes | No |
| ResultProgressInteractor | No | Yes |
| ProgressInteractor | No | No |
An example might look like this:
myFileDownloadInteractor
.receiveProgress((progress) => println("Downloaded ${progress}% of the file."))
.timeout(const Duration(seconds: 30))
.before((input) => println("Downloading file from ${input.sourceUrl} to ${input.destinationFilepath}."))
.after((_) => println("Successfully downloaded file."))
.intercept((exception) => println("Failed to download file. Exception caught: $it"))
.eventually(() => println("Finished downloading file from."))
.getOrNull(FileDownloadInteractorInput("https://example.com/file.txt", "/path/to/file.txt"))
Examples of all available modifiers #
The following section contains a bunch of examples covering all available modifiers provided by this library.
To avoid defining the interactor's used across all examples, they'll be defined in a code-block underneith the examples.
before #
final greetings = await greetName
.before((name) => 'Called with $name')
.getOrThrow('John');
after #
await greetName
.after((output) => print(output))
.run('Barney');
busy-state #
await greetName
.watchBusyState((isBusy) => print('IsBusy: $isBusy'))
.run('Josh');
eventually #
await stringToInt
.eventually(() => print('Conversion done'))
.run('10283');
intercept #
await stringToInt
.intercept((exception) => print('Exception: $exception'))
.run('not-a-number');
typed-intercept #
final result = await stringToInt
.typedIntercept<FormatException>((exception) => -1)
.typedIntercept<OverflowException>((exception) => 0)
.getOrNull('not-a-number');
checked-intercept #
final result = await stringToInt
.checkedIntercept((exception) {
if (exception.message.contains('42')) {
return true;
}
return false;
})
.getOrNull('not-a-number');
log #
await stringToInt
.log(
tag: 'String-To-Int',
logBefore: printInfo,
logAfter: printSuccess,
logError: printError
)
.run('12');
map #
final pi = await stringToInt
.map((output) => output + 0.1415)
.getOrThrow('3');
measure #
final result = await stringToInt
.measure((duration) => print('Conversion took $duration'))
.getOrThrow('19272');
recover #
final result = await stringToInt
.recover((exception) => -1)
.getOrThrow('not-a-number');
typed-recover #
final result = await stringToInt
.typedRecover<FormatException>((exception) => -1)
.typedRecover<OverflowException>((exception) => 0)
.getOrThrow('not-a-number');
checked-recover #
final result = await stringToInt
.checkedRecover((exception) {
if (exception.message.contains('42')) {
return true;
}
return false;
})
.getOrThrow('not-a-number');
run-at-least #
await synchronizeData
.runAtLeast(const Duration(seconds: 3))
.run();
timeout #
await synchronizeData
.timeout(const Duration(seconds: 10))
.run();
Interactors used in the examples above. #
GreetName #
final class GreetName implements ParameterizedResultInteractor<String, String> {
@override
FutureOr<String> runUnsafe(String input) async {
return 'Hello, $input';
}
}
StringToInt #
final class StringToInt implements ParameterizedResultInteractor<String, int> {
@override
FutureOr<int> runUnsafe(String input) async {
return int.parse(input);
}
}
SynchronizeData #
final class SynchronizeData implements Interactor {
@override
FutureOr<Unit> runUnsafe(Unit input) async {
// Perform sync ...
return Future.delayed(const Duration(seconds: 1), () => unit);
}
}