When I thought about solving this problem that I had experienced, I wanted to bear a couple of things in mind. First of all, I have no idea if it will catch on — will people us it? Do they already have their own organisations that they are committed to? Will it actually help the discovery of new groups and places to be involved with? These are a lot of core questions and whilst I could do some research, for a small side project I kind of just wanted to crack on and build something out. Another thing is the ability for it to be accessible from both platforms —a lot of people in this area I’ve met have mixed mobile platforms. Whilst I could build an iOS app in Swift, committing this time to two native experiences without this research (and with limited time) would mean that the workload would be significantly increased. For these reasons in mind, I decided to put something together using Flutter — this helped me to solve the problem quickly and build and app that was accessible to both platforms with ease.
You can download the app from either of these locations:[App store is currently under review for the iOS release]
And look at the source code here:
Note: If you intend to build the source code then you will require your own Firebase project, as the firebase configuration could not be open-sourced for security reasons.
I want to now take a little time to run through the project so that I can share how the application is built to serve both Android and iOS.
When it comes to platform specific widgets on Flutter there tends to be mixed opinions on what is the ‘right way’ to do things. However, it’s important to choose away which allows you to get things done and works for your project. For this app I’ve implemented a simple Platform Widget take on things — this allows us to use a simple abstract class which allows us to enforce the implementation of both an Android and iOS widget, returning the correct one accordingly:
Pretty simply huh? But how can we make use of this to drive our application? Well with that in mind, let’s take a quick look at how the application is structured:
As you can see from the above diagram, we have a collection of widgets that extend from a Platform Widget class. This class is abstract and allows us to provide a way to define Widgets that will support both an Android and iOS implementation — this approach has allowed me to take the need to provide native components for each platform, and decouple that responsibility into a single widget. Let’s take a quick look at one of the simpler widgets here as an example:
As you can see, the classes that make use of this button don’t know anything or even care about the platform differences — they just know that a button is being provided to be rendered on screen. Which with this solution is exactly what we wanted to achieve here. With all of that in mind, let’s run through each part of the app briefly so that we can get an idea of how this is all pieced together.
Note: Some code will be omitted here to keep this article easy to follow. I’ll provide links to the relevant Github files in each section for the curious!
The category data model is a core part of the application — this is essentially the firebase document which we are wanting to retrieve data from. In the file for the Category class we just define a collection of the different supported categories — these being firebase document references for animal rights groups, hunt saboteur groups, animal shelters / sanctuaries and global organisations. This gives us a centralised and enforced way of accessing these categories throughout our app.
For each category, the results returned by the document reference query can be filtered by country. Currently this will be either the United Kingdom, United States or Other — the user will be able to view this subset of content by making use of the tabs within the navigation / app bar at the top of the screen.
If you’re familiar with Flutter development then you might already know that we need to launch an application instance — here we do this in the form of a MaterialApp widget. I know what you may be thinking — “Material? But, aren’t we trying to optimise for the iOS experience here too?”. It’s OK! The MaterialApp widget won’t interfere with the transitions or styling of the application on iOS devices. All we do here is setup some simple styling configurations and assign our Dashboard() widget as the home property of our MaterialApp instance.
The Dashboard widget is essentially the home screen of our application — it’s used to declare the main body of the app, as well as the navigational components that are displayed here. Whilst some code has been omitted below, the core parts are the use of the PlatformParent and PlatformContent widgets:
Our DefaultTabController is only really used by the Material setup here, but it doesn’t mess with our Cupertino implementation. Our Platform Parent is the parent widget of our entire home screen — we have this so that we can contain everything that isn’t part of any content to be displayed. The platform parent is going to handle the display of the native tool / navigation bars for each platform which will each control the tab bar / segmented controls used to switch between the different filters for countries. The parent widget will also contain the bottom navigation implementations for each platform — so our parents is essentially going to contain the different types of navigation used to drive the content that is being shown on screen.
As mentioned in the section above, the platform parent is the widget that is used to contain the navigational components of our dashboard widget — this is both the bottom navigation bars and the tabbed navigation. Here we extend from our platform widget abstract class so that we can easily provide a native implementation for Material and Cupertino.
In the code below we start by providing a Scaffold widget for our Material implementation. This scaffold contains an AppBar widget, a TabBarView for showing our content body and a bottom navigation bar for the base navigation of our application. For the Cupertino implementation we have a similar setup — we provide a CupertinoTabScaffold which contains a tab bar for the bottom navigation, a navigation bar and a segmented control bar which will be used to drive the displayed content.
Within this widget you may have noticed a number of things going on. Let’s take a look first at how things work on the Material side of things:
We begin by taking a Scaffold widget and assigning it an app bar and bottom navigation widget. The Tab Bar takes two things into it — the first being tab widgets which are used to display the actual tabs, and second is the tab bar view — this takes a collection of content widgets and automatically switches between the children based off of the currently selected tab index. For the tab widgets we use a Platform Tab implementation as the widgets for the Material and Cupertino tab bars differ, so we offload this responsibility into a separate class.
The main difference when it comes to these two is that the Material setup takes a collection of content widgets, whilst the Cupertino setup takes a single content widget.
The content widget is used to generate the content area of the application, whilst we don’t style anything differently here — we have to generate the content collection differently as the content parent widgets for each platform take different arguments due to their setup.
You’ll notice here that for Android we return a collection of DocumentContent widgets, this is because the Tab Bar View setup in the parent widget takes a collection of children. However for iOS, the CupertinoTabScaffold tab builder only allows for us to pass in a single child for the body. We make use of this Platform Content widget here so that this logic is offloaded into a separate class, helping to keep the parent class cleaner.
The document content widget doesn’t need to handle any platform specific things as it simply just fetches data from Firebase and displays it to the user. The platform specific fonts and interactions are used by default. You can check out the full source here.
So because each platform uses its own widget type for its tabs, we need to provide an implementation for each. The TabBar used for material style components takes Tab widgets which hosts its own independent child, whilst the CupertinoTabBar style tabs just use a general widget.
The loading widget itself is just a simple component that shows some text and a progress spinner beneath it. The text consists of a predefined quote that matches up with the selected bottom navigation item, whilst the progress spinner is the progress indicator to match the platform implementation, using PlatformProgress.
Just like our other native implementation widgets, we use a platform progress widget to return us a progress indicator implementation specific to the platform that is in use. In this case we simply return a CircularProgressIndicator for Material themes and a CupertinoActivityIndicator for when the Cupertino theming is required.
The error widget is used in cases where the loading of data might not have gone quite as expected. The widget itself is pretty simple — it displays an error message along with a button, which uses the passed in onTryAgain argument to trigger the content refresh through the content widget.
The button used within the error widget is a platform button as buttons used for Material and Cupertino based theming have platform specific styling to them. This class itself is pretty simple as all we do is return either a MaterialButton or CupertinoButton depending on the theming which is in use.
And to conclude…
I hope the Voice app, combined with this article, provide a good resource as to how we can create completely native experiences for both Material and Cupertino with Flutter. Whilst some applications may take on their own complete style, or wish to use Material across the board, some builders may wish to make use of (or be influenced by) the native implementations for each component.
Personally for this application, I have found this approach to work. However, it would be interesting to see how it scales. What if the application was much larger, or what if there were 5 different platforms to support, would the codebase become too complicated to understand?
If you have any questions or thoughts on the above, then please feel free to reach out!