Extracting reused components to a Framework on both Android and iOS.
When building the Activity Filter app I was very much aware that the basic functionality (of what is a very simple app) had already been built within the more complex Training Plan.
This was the case in the case of both the Android and iOS versions of the app.
I did not want to reinvent the wheel, nor did I want to maintain two different codebases for the same basic functionality - displaying and filtering a users activities.
I opted to pull out this functionality into a framework for each respective platform.
What to pull
Whereas many frameworks only contain simple reusable view components, I wanted to extract more complete pieces of functionality.
Views.
- On Android this meant extracting layout xml files within my layout
directory.
- On iOS this involved extracting my views from my storyboards into independent XIB files (with corresponding UIView subclasses) and then using them as appropriate within each individual app project.
The general process was based on this answer by Ben Patch over at Stack Overflow. You create a XIB, set the files owner (in Interface builder) to a custom subclass of UIView
that implements NibLoadable
, hook up your outlets, and you are done.. sort of.
Whilst this method does allow you to see your view in interface builder, it is not editable from the location where it is embedded and adds a little bit of complexity in that you have to connect up a reference to your custom UIView
within your view controller, and access the various outlets through that.
Arguably this is better - a clear separation of concerns but it does have the downside that you have to manually set up your actions on your view elements within your controller. For example:
verifyYourEmailView.verifyEmailButton.addTarget(self, action: #selector(onClickVerify(_:)), for: .touchUpInside)
Controllers
- On Android we had to extract
Activities
andFragments
to our Framework. In the Training Plan app I had previously taken a fragment based approach whereby (massively simplified) every different screen was simply a different fragment displayed within a single activity.
This was always a poor way of doing things. The most obvious reason (that I encountered) being that fragment back stacks make navigation and view restoration unnecessarily complex. One of these issues is outlined in this blog post.
Over time I had developed complex custom backstack handling to avoid such issues but it was all a bit contrived and pointless given that with Activities everything just works..
It was your typical single developer technical debt shrug.
Anyhow, I took the opportunity to convert a lot of fragments to their own activities which were then extracted.
- On iOS we extracted some
UIViewControllers
in which the core of the apps business logic is housed.
Models
A ThirdPartyActivity
(an activity imported from a third party service like Garmin or Strava) is the same in the context of Training Plan and Activity Filter. We extracted reused models in both the Android and iOS app.
Assets
- Android. Easy.
- iOS. Easy enough, but i had to duplicate some things like
.xcassets
folders within both the framework and the individual app projects to get everything playing nicely with interface builder etc.
Fortunately, as outlined here app thinning within the app store process means that your assets are not included multiple times within the final bundle.
There were however some issues with handling named colors defined within the framework.
3rd party frameworks
Networking is the most obvious place where a third party library is used on both Android and iOS. Some of our extracted business logic interacted with our API directly so the framework needed to know about these libraries.
- On Android we use Retrofit for our networking, so our Framework needs to implement these dependencies itself within its
build.gradle
. It can not assume that these libraries will be included within the main project that is using it (because whilst they are, they might not be..).
This was super simple - it simply required implementing the dependencies within both the framework and the individual app projects. For example:
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
The Android build process handles everything. Deduplication etc.
- On iOS we use Alamofire alongside AlamofireObjectMapper. We use Carthage for our iOS dependencies, and getting all the libraries working correctly was quite a lot of hassle.
This was not necessarily because it was difficult (on paper) but rather because the latest version of XCode (12 at the time) was not being friendly with the version of Carthage available at the time. If I recall correctly, with the release of Apple's own in house silicon there were issues with the architectures that Carthage was building for and what Apple was now expecting/requiring. The resolution was to build xcframeworks
but the version of Carthage at the time was not able to.
The Carthage readme (for the latest version) now outlines how to build platform-independent XCFrameworks - carthage update --use-xcframeworks
. I built xcframework
s and then I imported them in both the framework and each individual project.
The trick to get things building correctly was to select 'Do Not Embed' within the framework whilst selecting 'Embed & Sign' in the individual app projects.
As you can see we rely on a number of 3rd party frameworks, and this was after I had taken the opportunity to clean up.
Build Settings
- The only build settings of sorts that seem relevant to discuss in relation to Android was the utilisation of properties defined within the individual apps within the frameworks codebase.
For example the framework interacts with our API so we had extracted all our API communication functionality to the framework. We wanted to pass an application identifier with each API request that was defined in each project.
The resolution was setting resValue
s in our build.gradle. For example:
resValue("string", "APPLICATION_IDENTIFIER", "\"TrainingPlan\"")
We used resValue
because as outlined here:
You can't override build configs unfortunately. You can override resource values specified with resValue
. The app can override the library, not the other way around.
- As regards iOS..
Whilst I accept that this is thoroughly unhelpful I am going to document this here for my own future reference. There was a period when I couldn't get my iOS projects to build correctly or I could, but then when I uploaded them to the AppStore for release I would get errors emailed to me such as the following:
If I recall correctly the 'Embed' type of my frameworks (outlined above) was an issue, but there were some Build Settings that also contributed to the problems.
On such setting that I recall was that my framework needed to have Build Libraries for Distribution
set to Yes
.
Subclassed activities
Having pulled out various view controllers into my framework, I chose to create open
them up and extend them within my app project such that any functionality could be reused or extended in the future.
In some cases, whilst the interfaces and the actions are reused the flows are sometimes different. For these cases I made my bases classes abstract
and then defined functions to be implemented within the subclasses. For example within our account creation flow the 'Create account later' button goes to a different, non-reused place in each app. This logic is implemented within the abstract function implementations.
Importing our Framework
Initially I had some issues with importing/including my Framework within the main app project. I was extracting the reused code into its own project within an independent directory structure. The missing piece of the puzzle was the specification of the frameworks source directory within the apps settings.gradle
:
include ':TPUserFramework'
project(':TPUserFramework').projectDir = new File('/Users/thomasclowes/Development/TPUserFramework-Android/TPUserFramework')