Photo by Artem Beliaikin from Pexels
Something that can really slow down app development over time is the large amount of simple, insignificant tasks that we do every day. Minor things that add up to long hours of extra effort if we neglect to automate them. It can be rough and I’m positive you too can relate if you:
- Keep hooking up QA devices to your computer to check Logcat for some weird issues.
- Use Postman to see what changed in an endpoint that worked just fine the day before.
- Entered the login information of your favorite test account after every clean install.
- Cleared the app’s local storage or cache several times a day.
- Waited minutes for new builds to compile just to adjust a minor detail.
Furtunately (🐶) Beagle, a recent pet project of mine is here to tackle these issues. After a quick setup it can make life easier both for developers and for QA engineers as well.
What is Beagle? Very simply put, it’s a side drawer added to your internal builds, containing a list of modules configured by you. These modules expose various controls and information boxes like feature toggles, radio groups, network activity logs, etc. Let’s start from the beginning.
Adoption
To start using Beagle, first make sure that Jitpack is added to your project’s repositories:
allprojects { | |
repositories { | |
… | |
maven { url "https://jitpack.io" } | |
} | |
} |
Having a different implementation for release builds makes sure that Beagle will not be available to regular users and neither will its code be part of your final APK— you can call the functions from the library so the app will compile, but under the hood none of them will do anything.
The last step of initializing Beagle is calling Beagle.imprint()
with an Application instance as the first parameter (pawrameter…? 🤔), before the first Activity is created. The onCreate()
method of the Application class would probably be a perfect place for it. Beagle.initialize()
will also work if you’re not a fan of dog-related puns.
class YourApplication : Application() { | |
override fun onCreate() { | |
super.onCreate() | |
Beagle.imprint(this) | |
} | |
} |
After this, every Activity in your app will have a side navigation drawer that you can summon by swiping from the right side of the screen, or by calling Beagle.fetch()
(that’s Beagle.openDrawer()
for serious people 🤫).
If you want to properly handle the behavior of the back button, there is one more step: see this example on how to override the onBackPressed()
callback of your Activities so that the library can consume the event when needed.
Training
Now that you installed Beagle, you should go on to teach him a few tricks specific to your app to take full advantage of its capabilities.
You can add tricks (modules) using Beagle.learn()
and remove them with Beagle.forget()
. Each trick has a unique ID, but you only need to worry about that if you want to dynamically remove them in the future.
ID-s are also useful for positioning modules relative to each other (again, only when they are dynamically added). Not all ID-s are provided by you, as some modules can only be added once and they have hardcoded identifiers. When this is the case, you can always access them as constants (Trick.Name.ID
).
The simplest way to train Beagle is to set up a list of static modules right after initializing the library. These will be the parts of the drawer that never change. Dynamic modules are only useful in case you want to add some tricks that are specific to a certain feature. A good example would be a login helper module that should only appear while the user is on the corresponding login screen.
Providing a list of tricks always resets the drawer while providing a single trick with an optional positioning parameter adds it to the existing list.
Check out the project’s GitHub page to see examples of setting up modules in a static or a dynamic way.
The boring API for this functionality contains three functions: Beagle.setModules()
, Beagle.putModule()
and Beagle.removeModule()
.
Tricks
Instead of describing every single module that is supported by the library, let me just present a few specific examples that I found extremely useful. Scroll down to the bottom of the article to see how the end result looks in action.
Header
This module can be used to provide information about the build, but it requires some configuration. My usual setup looks like this:
Trick.Header( | |
title = getString(R.string.app_name), | |
subtitle = "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", | |
text = "Built on ${BuildConfig.BUILD_DATE}" | |
) |
This will display the name of the app, the version name and the version code as well as the date when the build was created. For the latter to work, a custom BuildConfig field needs to be added to your app-level Gradle build script like this:
android { | |
… | |
defaultConfig { | |
… | |
buildConfigField("String", "BUILD_DATE", "\"${new Date().format("yyyy.MM.dd")}\"") | |
} | |
} |
When added to the drawer, the Header module will always be the first one, regardless of the specified positioning.
NetworkLogList
A very useful module that not only displays a collapsable list of incoming and outgoing network activity logs but also allows you to see the details of every item in a separate dialog, like the formatted JSON payload, the request duration or the list of headers.
Setting it up is very simple, as all the parameters are optional. However, providing the base URL is recommended so that repetitive content can be filtered from the UI:
Trick.NetworkLogList( | |
baseUrl = NetworkingManager.BASE_URL, | |
shouldShowHeaders = true, | |
shouldShowTimestamp = true | |
) |
This trick will only work if you add the BeagleNetworkInterceptor
object to the Builder of your OkHttpClient
as shown here.
AppInfoButton
A very simple shortcut that will directly open Android’s Settings app with your application’s App Info page. Useful if you frequently need to change permissions / clear cache / clear storage / etc.
ScreenshotButton
When pressed this button will take a screenshot of the app (without the drawer of course) and open a share bottom sheet so that the image can be immediately forwarded. The files are saved into the app’s cache folder (no storage permission is needed).
SimpleList
This trick displays a collapsable list of items and invokes a custom callback function when the user taps on one of them. It’s ideal for hardcoding a list of test accounts for an authentication screen. You just have to create a model implementing this interface:
data class Account( | |
override val name: String, | |
val password: String | |
) : BeagleListItemContract | |
val testAccounts = listOf( | |
Account("User 1", "password1"), | |
Account("User 2", "password2"), | |
Account("User 3", "password3") | |
) |
…and provide a list of such items together with the callback function.
Trick.SimpleList( | |
id = TEST_ACCOUNTS_MODULE_ID, | |
title = "Test accounts", | |
items = testAccounts, | |
onItemSelected = { account -> | |
findViewById<EditText>(R.id.username_input).setText(account.name) | |
findViewById<EditText>(R.id.password_input).setText(account.password) | |
Beagle.dismiss(this@LoginActivity) | |
} | |
) |
When implementing callbacks, we’re essentially keeping a reference to the instance where the lambda is implemented so in this case, not wanting to leak the Activity, a module ID is also provided which will later be used to remove the module from the drawer.
SingleSelectionList
This module has an almost identical setup to the SimpleList trick, but a slightly different behavior: it displays a list of radio buttons and makes sure that only a single item is selected at any given time. You can also specify a default selection which is useful if you want to persist the user’s choice in SharedPreferences
for example. A good example use case would be switching between different backend environments:
Trick.SingleSelectionList( | |
title = "Backend environment", | |
items = environments, | |
isInitiallyExpanded = true, | |
initialSelectionId = preferenceManager.environmentId, | |
onItemSelectionChanged = { environment -> changeEnvironment(environment.id) } | |
) |
LogList
This might be useful for anything you used Logcat for. You just have to call Beagle.log()
to push new entries to the stack. You can even specify a tag for each element and add different LogList modules that for each of the tags you’re using. The advantage is that the logs appear directly on the UI and if you need to provide longer pieces of information for each entry, the contents of a third parameter (payload) will be displayed in a dialog when the user taps on an item.
Integrating this feature into an AnalyticsManager
class can make testing the tracking functionality a lot easier.
KeylineOverlayToggle
When the toggle is enabled, this trick will draw a grid over your app’s UI that can help you verify the alignments of the Views. If no parameters are provided, the grid will have the default keylines specified in Material design: a baseline of 8dp with bolder lines at 16dp and 72dp (24dp and 80dp on tablets).
DeviceInfoKeyValue
This module shows information about the device the app is running on, such as the Android SDK version, screen resolution, etc.
With the large variety of supported tricks you can probably think of other use cases as well. Do let me know if you come up with something interesting!
Costumes
The appearance of the library can be personalized by specifying a second, optional parameter when initializing it. The most important customization option is probably setting a theme resource ID for the drawer (by default every drawer will be inflated using the corresponding Activity’s theme).
This file shows some of the relevant attributes, but as the library uses standard Android widgets, you should be able to customize them using the usual styles API (no custom attributes are introduced by the library).
Throw me a bone
Beagle is in early stages of development (one might say it’s a bit ruff around the edges) but it’s ready for day-to-day use. If you like where it’s going, check it out on GitHub and maybe star it as well. There you can also find a full example app where the usage of every trick is demonstrated as well as a more detailed documentation with screenshots.
I’m not sure where you’re getting your info, but great topic.
I needs to spend some time learning much more or understanding more.
Thanks for fantastic info I was looking for this info for my mission.