Requirements*
Small utility library for Android to evaluate requirements in order for some action to proceed. For example: network connection, permissions (API 23), system services (location, bluetooth, ...), etc.
implementation 'ru.noties:requirements:1.1.0'
Overview
In order to correctly react to a user action we must make sure that all requirements are satisfied for this action to proceed. Sometimes it's just network connection. Sometimes it's location services and, on devices starting Marshmallow, the location permission. The list goes forth. So does the code in an Activity or a Fragment.
The aim of this library is to detach requirements' resolution process from the code and give flexibility to add/remove requirements on-the-go.
final Requirement requirement = RequirementBuilder.create(EventDispatcher.create(this), eventSource)
.add(new NetworkCase())
.addIf(BuildUtils.isAtLeast(Build.VERSION_CODES.M), new LocationPermissionCase())
.add(new LocationServicesCase())
.build();
The pivot point of this library is the RequirementCase
. It encapsulates one single case that must be met before an action can go further. It will receive onActivityResult
and onRequestPermissionsResult
events and can react to them if needed. In general: RequirementCase
is state-less container that validates if requirement case is met and, if not, starts resolution.
There are 2 API methods in RequirementCase
that must be implemented:
boolean meetsRequirement()
void startResolution()
For example in case of network connection one might do this:
@Override
public boolean meetsRequirement() {
final ConnectivityManager manager
= (ConnectivityManager) appContext().getSystemService(Context.CONNECTIVITY_SERVICE);
final NetworkInfo info = manager != null
? manager.getActiveNetworkInfo()
: null;
return info != null && info.isConnected();
}
If meetRequirement
returns false
, then startResolution
will be called
@Override
public void startResolution() {
new AlertDialog.Builder(activity())
// configuration of a dialog
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
startActivityForResult(
new Intent(Settings.ACTION_WIRELESS_SETTINGS),
requestCode
);
})
.show();
}
@Override
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
if (this.requestCode == requestCode) {
deliverResult(meetsRequirement());
return true;
}
return false;
}
Event dispatcher
In order to decouple a RequirementCase
from Activity
EventDispatcher
type was introduced. It is used to dispatch these events:
- startActivityForResult
- requestPermission
- checkSelfPermission
- shouldShowRequestPermissionRationale
Library provides 2 implementations:
EventDispatcherActivity
-EventDispatcher.create(Activity)
EventDispatcherFragment
-EventDispatcher.create(Fragment)
Event source
In order for RequirementCase
to react to Activity events, EventSource
must be used. Obtain an instance of it by calling: EventSource.create()
. Then redirect onActivityResult
and onRequestPermissionsResult
to it. Each method returns a boolean indicating if event was consumed.
private final EventSource eventSource = EventSource.create();
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (!eventSource.onActivityResult(requestCode, resultCode, data)) {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (!eventSource.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
Please note that if an
EventDispatcher
is established viaFragment
(thus dispatching events via Fragment methods), EventSource must also be intialized inside that fragment (otherwise you won't receive any events).
Validation
It's a good thing to start validation process in reaction to some action (button click, etc).
requirement.validate(new Requirement.Listener() {
@Override
public void onRequirementSuccess() {
// can proceed now
}
@Override
public void onRequirementFailure(@Nullable Payload payload) {
// cannot
}
});
If requirements must be validated during some lifecycle event (onStart, etc), one can use requirement.isInProgress()
method call to check if requirement is currently in progress.
There is also a synchronous method isValid()
that returns true|false
and does not start resolution.
Cancellation
Requirement resolution can be cancelled by:
RequirementCase
by callingdeliverResult(boolean)
anddeliverResult(boolean, Payload)
- calling
Requirement#cancel()
andRequirement#cancel(Payload)
Cancellation payload
In order to react and take actions when requirement resolution was cancelled a simple type Payload
was introduced. It's a interface
with no methods defined to ensure type safety. It can contain data to identify specific cancellation case, which will be delivered to validation listener.
Dialogs in resolution
It's aboslutely crucial that after startResolution
is called RequirementCase
must deliver success or cancellation event (it can be postponed for example until onActivityResult
or onRequestPermissionsResult
is delivered). Otherwise the requirements chain will break.
In case of showing a dialog in startResolution
, it's advisable to track the dismiss state of a dialog. Library provides utility class Flag
that can help keep track of dialog state:
final Flag flag = Flag.create();
new AlertDialog.Builder(activity())
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// mark as success
flag.mark();
// for example, start activity for result
}
})
.setNegativeButton(android.R.string.cancel, null)
.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
if (!flag.isSet()) {
deliverResult(false);
}
}
})
.show();
Activity lifecycle
Internally Requirement
listens for supplied activity lifecycle events and disposes itself when attached Activity went through onDestroy
. If Activity was destroyed whilst requirement resolution process is still in progress that process will be lost.
Multiple listeners
Please note that if a Requirement#validate
is called and Requirement#isInProgress
is true, supplied listener won't trigger the whole validation process again but instead will subscribe for the final result (with other listeners).
Request code
Sometimes our creative abilities give us a hard time and we sit hours thinking of ideal request code:
private static final int LOCATION_PERMISSION_REQUEST_CODE = 72;
It's great time and I won't trade it for anything. Still, if you wish, there is an utility class to do this amazing job for you:
RequestCode.createRequestCode(String);
RequestCode.createRequestCode(Class<?>);
Permissions
To deal with Android runtime permissions (introduced on devices starting API 23), there is a base class to help with them - PermissionCase
. It requires only one method to be implemented: void showPermissionRationale
.
public class LocationPermissionCase extends PermissionCase {
public LocationPermissionCase() {
super(Manifest.permission.ACCESS_FINE_LOCATION);
}
@Override
protected void showPermissionRationale() {
final Flag flag = Flag.create();
new AlertDialog.Builder(activity())
.setTitle(R.string.case_location_permission_title)
.setMessage(R.string.case_location_permission_rationale_message)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
flag.mark();
requestPermission();
}
})
.setNegativeButton(android.R.string.cancel, null)
.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
if (!flag.isSet()) {
deliverResult(false);
}
}
})
.show();
}
}
If it's required to check if user selected never
on permission request dialog, there is also a way to deal with it (by default it delivers cancellation result):
@Override
protected void showExplanationOnNever() {
final Flag flag = Flag.create();
new AlertDialog.Builder(activity())
.setTitle(R.string.case_location_permission_title)
.setMessage(R.string.case_location_permission_never_message)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
flag.mark();
navigateToSettingsScreen();
}
})
.setNegativeButton(android.R.string.cancel, null)
.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
if (!flag.isSet()) {
deliverResult(false);
}
}
})
.show();
}
Aside from that PermissionCase
offers these helper methods:
void requestPermission()
void navigateToSettingsScreen()
- useful when implementing permission logic on user selectednever
License
Copyright 2017 Dimitry Ivanov (mail@dimitryivanov.ru)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.