This document will serve to help developers wanting to contribute to the project by covering things like app architecture, third party libraries used for major functions, as well as how the app interacts with the Elrond blockchain.
It's highly encouraged to follow the existing coding standards which has not been formerly defined, but the project pretty much follows the SOLID Principles as well as the DRY (Don't Repeat Yourself) pattern. Keeping this in mind will help keep the app maintainable and easy to follow for other developers.
The architecture is typical of your standard Android project. It also employs the factory design pattern where most objects are managed by the IoC container. It uses Koin for dependency injection.
- Android Studio
The project has two modules - app and data
The app module is typical of any standard Android project. It holds all files that have to do with UI, such as fragments, layout files, view models, etc, as well as any Android specific logic such as logic that needs to interact with the Android SDK.
The data module should only be concerned with network access and data structures, or any helper code that has nothing to do with the UI. The data module should never reference anything from the app module.
-
In the app level build.gradle file under signingConfigs you will find the reference to the keystore details. It’s highly recommended you move the password to an environment variable, or more preferably a CI variable in your pipeline.
-
Under defaultConfig you will find the versioning scheme using the four variables - major, minor, patch, hotfix. There’s a method in there that creates the proper version code based on those variables. Always increment prior to uploading a new build to the Google Play Console.
When running the app locally, it's recommended to use the internalDebug variant since that is already enabled for debugging and it also will be pointed at the Devnet chain. If you select productionRelease the app will be pointed at Mainnet so use caution when selecting this option.
To package the app to upload to the Google Play console from Android Studio follow these steps
- From the Build menu select Generate signed bundle/apk
- Select App Bundle
- Fill in the keystore details (you can find the correct values to enter in the build.gradle file as mentioned above)
- Choose the correct build configuration (internalBeta if you want the app to use devnet. productionRelease if you want the app to use Mainnet)
- Open the Event Log and click the Locate link to find the aab file you just built
- DataBinding is used for most, if not all, layouts
- RecyclerViews use Groupie which makes for seamless recycler view setups
Groupie is a simple, flexible library for complex RecyclerView layouts.
It’s important to call out a few gotchas when using this library. Let’s take a look at an example that already exists in the app — the Transaction History screen. These are the files involved:
- TransactionHistoryFragment
- TransactionHistoryViewModel
- TransactionItemViewModel - instead of setting several variables in the data block of the layout file, this single object has all the data we need, then our data labels bind to the properties in this class
- TransactionItem is responsible for binding the data for each individual transaction item, and is required for Groupie configuration (explained below in more detail)
- transaction_item.xml - the layout file for our transaction item
The fragment will set up the recyclerView that will display all our transactions, and all we need to do is create a layout manager and set the adapter to a GroupAdapter<GroupieViewHolder>. This will work in most cases, more complicated recycler view layouts might require more setup.
private fun setupRecyclerView(recyclerView: RecyclerView) {
recyclerView.apply {
layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.VERTICAL, false
)
adapter = GroupAdapter<GroupieViewHolder>()
}
}
The view model retrieves the transaction data from the blockchain and builds a list of TransactionItemViewModel objects with the data we want to display in each adapter item. The view model sends that list back to the fragment and here’s what happens next…
The groupie adapter needs to work with something called a BindableItem class. So we create our own TransactionItem class that extends BindableItem<TransactionItemBinding>, then override the required methods:
-
bind()- use this method to set variable values in your xml layout, in this example we have a “vm” variable that we set the TransactionItemViewModel to. You can also change UI state directly via the viewBinding object passed in. In this example I change the transaction status label text color based on the transaction status value -
getLayout()- this should return the xml layout file for the transaction item -
initializeViewBinding()- this should return the result of DataBindingUtil.bind(view)
TransactionItemBinding is a generated class that represents our layout file, same as our fragment bindings. So the layout xml file needs to be created first before the TransactionItem class can be finished otherwise the TransactionItemBinding would not exist yet.
Back to the fragment. Once we have the TransactionItem class filled out, we map the list of TransactionItemViewModel objects sent back to the fragment into a new list of TransactionItem objects and that list is what we ultimately set the recyclerView’s data items with.
viewModel.transactionItemModels.observe(viewLifecycleOwner) { models ->
val items = models.map {
TransactionItem(requireContext(), it)
}
binding.recyclerView.setDataItems(items)
viewModel.showLoading.postValue(false)
binding.swipeRefreshLayout.isRefreshing = false
}
Refer to AppModules to see how we set up our dependency structure. For example, we register view models using the viewModel dsl extension and pass whatever dependencies we need into the view model constructor (such as repository interfaces). The view model instance can then be referenced in the associated fragment like this:
private val viewModel: CreateAccountViewModel by inject()
The Chartr app is a single activity app, meaning there is only the MainActivity. Well sort of, the onboarding flow uses its own activity but once onboarding is done that activity finishes and the rest of the app uses the MainActivity throughout. So since onboarding is done only once for a user (as long as they complete it without force quitting the app) after they install the app, I still consider it a single activity app.
With that said, each screen should have its own fragment and view model class. So when creating a new fragment select the option with viewmodel as shown below 
Also DO NOT name the layout like “fragment_edit_account” because the databinding framework will generate classes that represent this layout and you will be referencing these classes in code, so you don’t want to generate a class called “FragmentEditAccountBinding”. Instead, name it something like “edit_account_fragment” so you’ll end up with a generated binding class called “EditAccountFragmentBinding” which is more readable and intuitive.
When making network calls or performing any long running asynchronous operation, always use the DispatcherProvider implementation with the viewModelScope.launch call to run a coroutine in the background. This allows you to easily specify which thread you want to run that block of code on (UI thread vs background thread). There are plenty of examples in the code on how to do this properly.
For example, this line will run the code block in a background thread
viewModelScope.launch(dispatcherProvider.IO)
While this will run the block on the main UI thread
viewModelScope.launch(dispatcherProvider.MAIN)
You may find yourself needing to modify the UI within a callback that was originally executed on the IO thread, in which case you need to launch a new routine on the MAIN thread and within that block you can access the UI without the OS barfing on you.
Network calls need to be done inside a coroutine as mentioned above. They should always be done from a view model class and from there call the appropriate repository implementation. There are some exceptions where you find you need to run something in the background from a fragment, in that case you just need to reference the viewModelScope off the viewModel instance in your fragment. Something like ….
homeViewModel.viewModelScope.launch(dispatcherProvider.IO) {
// Do Something
}
The call hierarchy for making network calls should go like this:
- ViewModel -> Repository -> Service -> REST client
The view model has an instance of whatever repository you want to use passed in its constructor via dependency injection. The repository in turn uses the correct service class passed into its constructor (ElrondApiService as an example) and that in turn calls the REST client get or post methods.
Navigation between fragments is configured in nav_graph.xml. If you need to navigate from a view model, there is an easy way to do it: From your view model class, create an event your fragment can subscribe to as follows:
private val _navigationEvent = LiveEvent<NavigationEvent>()
val navigationEvent: LiveData<NavigationEvent> = _navigationEvent
In the same view model file, add this logic wherever you want to trigger a navigation somewhere
_navigationEvent.postValue(NavigationEvent.Directions(
SecretPhraseFragmentDirections.actionSecretPhraseToVerifyWordsFragment()
))
So in this example we are navigating from the “secret phrase” fragment to the “verify secret phrase” fragment.
In your fragment, override the onViewCreated() method and subscribe to the event above, something like this:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.navigationEvent.observe(viewLifecycleOwner,
NavigationObserver(NavHostFragment.findNavController(this@SecretPhraseFragment))
)}
So where did the SecretPhraseFragmentDirections class come from? That class was generated based on the action block you define in the nav_graph.xml file, in conjunction with the element IDs.
The data module consists of all the logic that knows nothing about the UI or anything about Android in general. Theoretically this data module can be plugged into another android app with little to no modifications.
I had plans to make this its own github public repository, eventually it probably should be.
Refer to the DataModules class. This is where we create singletons of our repository, service, and http client classes. The logic in here is much more complicated than the AppModules.kt class above, particularly the networkModule block.
The main idea is that we inject the correct RestClient instance with the baseUrl for whatever service we are using. For example, the Elrond Gateway service has a different base URL than the Elrond API service. So if we want to call an endpoint on the Elrond API, we use the ElrondApiService class which is set up to accept the proper restClient instance that has the elrond API baseUrl. That way we only have to have one HTTP client class (the RestClient.kt class) to make all our web service calls in a generic fashion.
Repositories should be the only access point for making network calls from the UI. The interfaces are public but not their implementations, which is important to note because you can’t actually create a new instance of a repository concrete class from within the app module because they are marked internal, the interface has to be injected into whatever UI file you need to use it in, then it gets resolved into its concrete implementation by Koin whenever we ask for an instance of it.
All repositories are registered in the Koin IoC container, as mentioned above. All repositories in turn need to inject the proper service class in their constructor, again that is configured in the IoC container in DataModules.kt
The repositories should also be responsible for most of the business logic, doing null checks, catching exceptions, etc, and returning the correct data back to the UI.
This extra layer of separation between the repository and service classes also allows for better unit testing, something this library desperately needs.
Service classes are responsible for simply calling the GET/POST methods in the RestClient.kt class. As mentioned earlier, each service class is set up in the DataModules.kt class with the baseUrl already. So depending on the service class you want to use, it will have the correct instance of RestClient passed into the constructor.
- ElrondGatewayService has specific methods for calling endpoints on the elrond gateway (base url of https://gateway.elrond.com), while ElrondApiService has specific methods for calling endpoints that only exist on the elrond API (base url of https://api.elrond.com)
- AeroPlaidService - this was meant to be used with our Plaid backend, a NodeJS backend I built for use with the Plaid services This was POC’d out on iOS but not Android. This may no longer be the viable option for Chartr but it was the direction we were going.
- AviationEdgeService - This service doesn’t do anything yet, but was meant to query aircraft data via the Aviation Edge API
- PrimeTrustService This was meant to be used in conjunction with Plaid for the financial piece of Chartr - see more
The first thing you’ll notice is that there are two different versions of get and post methods apiGet() and apiPost() vs gatewayGet() and gatewayPost(). This is because the JSON response structure is different between the Gateway and the API, so it was easier to just create two different versions for each. This can definitely be improved but it works.
Most of the Elrond specific logic, such as data structures, cryptographic functions, etc, was taken from the ErdKotlin repo and restructured to fit our architecture better. Since it’s not actively maintained, I pulled the necessary pieces out and added the code to our data module so we can maintain it ourselves.
Note the elrondsdk.usecase package. This may have some helpful functions to use later, but note the deprecated warnings on all of the classes in there. Do not use these classes, they need to be converted into a repository class and set up like all the other repository classes so it fits the Chartr architecture.
- https://docs.elrond.com/sdk-and-tools/rest-api/rest-api/
- https://docs.elrond.com/sdk-and-tools/rest-api/gateway-overview/
- https://docs.elrond.com/sdk-and-tools/rest-api/api-elrond-com/
- https://developer.android.com/studio/build/build-variants#sourcesets
- https://kotlinlang.org/docs/reference/coroutines/flow.html
- https://github.com/bumptech/glide
- https://developer.android.com/training/camerax
- https://developer.android.com/jetpack/androidx/releases/navigation
- https://github.com/lisawray/groupie
- https://square.github.io/okhttp/
- https://github.com/InsertKoinIO/koin