Run one codebase but provide a multi-platform look!
Possibly, like myself, when you began learning Flutter, you yourself went through the lab exercise called, Write your first Flutter app. You may even remember, at the end when you completed both Part 1 and 2 of this exercise, you’re then greeted with this message:
I remember being somewhat disappointed with that message. Yes, indeed it runs on both an iOS phone and an Android phone, but on both phones, the app has only one ‘look and feel.’ Both used the Material design theme. If running on an iPhone, wouldn’t you have preferred the iOS-style Cupertino design theme instead? Of course, you’ve got to implement all that yourself and run the appropriate theme depending on the phone your running on.
Below are two screenshots. On the left, is the ‘Material theme’, and on the right, is the ‘Cupertino theme.’ On the left, is the chosen theme for Android phones. On the right, the theme more familiar to iPhones. Both are in the same codebase. In this article, I’ll show you how I can pick which theme to use no matter what type of phone. Wanna know how? Read on.
As in most of my articles, I use well-founded examples to demonstrate the topic at hand. In this article, as you may have already guessed, I’m going to use the very same “Write Your First App” example. However, this example will be modified to either convey the appropriate ‘look and feel’ depending on the platform it’s running on, or you can specify which look you would prefer when running it. You can get a copy for this from the following gist, write_your_first_app.dart.
I Like Screenshots. Click For Gists.
As always, I prefer using screenshots over gists to show concepts rather than just show code in my articles. I find them easier to work with frankly. However, you can click or tap on these screenshots to see the code they represent in a gist or in Github. Ironically, it’s better to read this article about mobile development on your computer than on your phone. Besides, we program on our computers — not on our phones. For now.
The Theme’s Design
First, a quick blurb about the two design themes we’ll be working with, today. The Material theme was designed by Google to emulate the physicality of paper. However, as described by its designer, Matías Duarte, “unlike real paper, our digital material can expand and reform intelligently. Material has physical surfaces and edges. Seams and shadows provide meaning about what you can touch.” It was introduced in 2014, intended initially for the Android platform, but would later migrate to iOS and even the Web. As for the Cupertino design theme, it’s named after the city of Cupertino in California’s Silicon Valley. The company, Apple, is headquartered there, and is, of course, responsible for the distinct ‘look and feel’ that is the Cupertino design theme.
A Theme Class
Each design theme has a dedicated library file to access their specific widgets:
cupertino.dart.Further, each has a class that serves as the foundation of their individual ‘look and feel.’ Like the CuperitnoApp class and its iOS-style design, Flutter’s MaterialApp class documentation describes, the MaterialApp as, “a convenience widget that wraps a number of widgets that are commonly required for material design applications.”
Of course, Flutter allows you to determine which platform your app is running on, and so base on that, you can easily apply a specific widget. For example, depending on the platform the ‘Write Your First App’ example is running on, either the MaterialApp widget or the CupertinoApp widget could be used. You can see this in the screenshot below. Again, as a rule, the MaterialApp widget is designated for the Android platform, the CupertinoApp widget is used on the iOS platform. As you see in the screenshot below, Flutter’s Platform class has static variables to determine the current operating system. If running in the iOS operating system, the variable, Platform.isIOS, is set true, and so the CupertinoApp widget will run. See how that works?
Of course, rules are meant to be broken! I’ve replaced that Platform variable with a particular getter called, useCupertino, which I regularly use in my apps. If it’s true, the CupertinoApp widget is used. However, if it’s false, the MaterialApp widget is used no matter the type of phone. Simple.
Let’s now take a look at this wonderous getter. Of course, I don’t copy n’ paste this getter into every new app I begin to work on. I’ve built up a collection of Dart library files in the form of packages over these many months that now serves as my standard toolkit. To be exact, it’s part of the framework I wrote to include with every project I work on. However, that’s for another article. In this article, I intend to merely apply this one bit of code to the example code so as to demonstrate a means for you to do the same with your apps. However, looking at that getter below, you see it’s a rather busy-looking boolean expression.
Best to illuminate where its parts come from so as to understand the whole. The first two private variables, _useCupertino, and _useMaterial, for example, come from the named parameters used at the start of the example app. A screenshot below points out where all three private variables, _useMaterial, _useCupertino, and _switchUI, get their values from.
There are two screenshots below showing the ‘main’ file,
main.dartwith a named parameter used in each. As a result of the value,
true, being passed to these named parameters, if running on an Android phone, for example, both would result in a Cupertino theme interface. The first one explicitly tells the app to use the ‘iOS-style’ interface, while the second one, no matter what platform the app is running on, will ‘switch’ to the opposite interface. Both convenient features when developing apps on one particular platform allowing you to test and develop the two interfaces.
Here you go. Below is a screenshot of both my ‘Material’ getter and my ‘Cupertino’ getter. Both have been placed in this example code. Again, I use either one regularly throughout my apps to control and test the many interfaces I make available all in one codebase. Nice.
It’s easier to just present a table indicating which design theme is used depending on which getter is set to true and which platform the app is running on. The two things to take away is, if both getters are true, it defaults to the theme usually used on the platform, and if it’s running on the Web, Material would be the default theme. You heard right…the Web.
That’s what that variable, kIsWeb, is all about. It’s provided by Flutter’s foundation library and is set to true if your Flutter app is running on a browser. I suspect like many of you, I’ve dabbled with running my apps on the Web, but again, that’s for another article. Let’s continue on with the example code and see what’s next.
What we’ll look at next is the ‘home’ widget designated to be displayed in this example app. After determining whether we’re going to use the
material.dart library or the,
cupertino.dartlibrary, the next bit of code is the StatefulWidget that makes up the ‘home’ widget. However, unlike the original version from the lab exercise, this version has an interesting variation. You can see one of those two getters is again utilized here. This time, it’s the getter, useMaterial . It’s used to determine which State object to use. By the State objects’ very names, you can deduce what happens if the getter, useMaterial, is set to true. If true, the State object, RandomWordsAndroid, is called. As you can guess, it is here where the app determines whether to display the Android’s ‘look and feel’ or the iOS ‘look and feel.’
Of course, I could have just as easily used the other getter,
MyApp.useCupertinoand switch the State objects around appropriately. Either way would do.
MyApp.useCupertino ? RandomWordsApple() : RandomWordsAndroid();
A Two-Part State
Notice I take advantage of the fact that StatefulWidget’s are made up of two separate components. It’s in the StatefulWidget’s createState() function where there’s a logical location for a conditional operator ( ?: ) to choose which ‘interface’ is to be displayed. A convenient circumstance.
A Stateless State
That’s all nice and good for StatefulWidgets, but how about StatelessWidget you might ask. Well, I asked that question as well with one of my projects but realized StatelessWidgets are rather ‘light’ by design. There’s little overhead to make up a two-part arrangement in a StatelessWidgets as well. In this case, I have a StatelessWidget calling another StatelessWidget. The screenshot below of one of my projects will reflect what I’m talking about. Since StatelessWidget’s don’t have the createState() function, the conditional operator, ?:, is instead found in the first StatelessWidget’s build() function. It calls, in turn, the appropriate StatelessWidget depending on the value of, App.useCupertino. Easy peasy.
The Android Version
The screenshot below is the ‘Android version’ of the State object. It’s not readily apparent here, but all the widgets involved in its build() function come from the library file,
material.dart.You’ll notice another difference from the original lab exercise version as well. All the ‘logic’ as it were, in other words, all the code responsible for generating the many names specifically, is no longer visible in this State objects build() function. It’s all now encased in a separate class called, _WordPair. Now, why is that? Of course, one of the first things you learned about programming is to reduce repeated code. Repeating such code, in this app, would be a nightmare to maintain and prone to errors. And so, as you may have guessed, the very class _WordPair used in the ‘Android’ State object is used in the ‘iOS’ State object as well.
Let’s place those two State objects side by side below. You can then readily see the differences and similarities between them. On the left, is the iOS version; the Android version on the right. In theory, all the widgets that make up the Cupertino version are from the library,
cupertino.dart while the Android version, again, uses only the library file,
Admittedly, having been an Android developer in a past life, I’m more attune to the Material design and not the Cupertino design. For example, I’m not familiar with a Cupertino version of the Divider widget — if, in fact, there is one. As a result, I’m using Material’s Divider class in both interfaces. Further, there doesn’t appear to be a Cupertino-counterpart for the ListTile widget. I had to find one, CupertinoListTile.
All By Design
As with separating the ‘platform code’ in my apps, I also tend to separate the data, the Interface, and the logic that makes up my apps following the Model-View-Controller design pattern. Far from an original concept, the MVC software design pattern was first introduced by Trygve Reenskaug while visiting the Xerox Palo Alto Research Center back in the 1970s. For some 40 years, it would be a prominent approach to software development. Of course, Google formally endorses the Provider architecture for separating aspects of your app into manageable parts. Why MVC in this case? Personal preference.
And so, instead of having the example code all in one file, for instance, you’ll find following some sort of ‘software design pattern’ like MVC will organize your code in such a fashion to make it much easier to then maintain and even change as time goes by. Further, following such an approach allows new developers to take over duties that much easier and that much faster. Let me show you what I mean.
Below is a screenshot of the ‘main entry file’ of the example again, but now much of the source code is found elsewhere, no longer making it one big monolithic program file. Following best practices in Dart, while the code in the directory,
lib/, is considered public and accessible to other Dart packages, code under the directory,
lib/src/,is private, and that’s where the rest of the example code now lives.
In fact, at a glance, you can see there are now three additional directories: controller, model, and view. If you’re familiar with the MVC design pattern, you’d guess correctly then the code responsible for the example’s data would be under the directory, model, the interface code would be found under the directory, view, and all the code that responses to user and system events (the logic) would be under the directory, controller. By the way, organizing your source code in such a way, also allows separate teams of developers to work in those separate areas in conjunction with each other, or when advantageous…separately — in theory, allowing for more efficient use of time and resources.
Opening up those directories now relieves all the source code that makes up the example app in this article. If you examine the directory names closely, in time, you’d probably figure out what each dart file is responsible for.
For example, we see below the library file,
main.dart,which now contains only what it needs to do its part. There’s no longer any extra code masking what this file is to accomplish. It’s cleaner. More efficient. Note, both the library file,
cupertino.dart,has access to the function, runApp(), however, I’m only using one of them. At this point, which one you use would be a personal preference. Also, I like using the ‘show’ clause in import statements. It’s so that, at a glance, I can then see what is used in a particular library file — again, personal preference.
So what else is going on in that file? Well, of course, the ‘MyApp’ widget is passed to the runApp() function. Now, where is that file? There is an import statement to ‘show’ where that widget is, of course, but you can see in the screenshot below that it’s under the directory, controller. It’s been placed there by design to describe its role in this example app. Now things are a little more organized. Let’s take a look at that file next.
Like the file,
main.dart,this file contains only what it needs to perform its role. As you know from reading this article, the MyApp class essentially establishes the foundation for the remaining widgets to be called from and thus displayed in this app. In most cases, this will be based on the platform this app is running on. Of course, there are now those named parameters: android, iOS, and switchUI to dictate the interface used. The logic of all this resides now in this one lone file. If, down the road, someone changes those parameters, for example, the rest of the code will not be affected — not directly anyway. The rest of the code will be none the wiser. Further, a team leader could now more readily control who has access to this file to make such changes. Nice feature, no? Nice and clean.
What else does that file do? Oh yes! It determines the ‘home’ widget to be called to display the app’s interface. The screenshot below highlights where that file resides. It’s under the directory,
view/home/. Make sense. It involves the app’s interface (view) and involves the ‘home’ widget (home), and so, that’s where it lives. Of course, the last import in the screenshot above conveys that location. Note, by convention, it’s discouraged to actually use the ‘relative path’ in import statements. The full notation starting with the package’s name is preferable, but since I just slapped this together as a supplement read to this article, please forgive my failing here.
And so, it determines the ‘home’ widget, and in fact, it’s in a file named,
home.dart. Unlike Java, its resident class does not have to be the same name as the file and is called, RandomWords. It’s in the screenshot below. Of course, we already know how it determines which ‘interface’ to display.
Again, we’re taking advantage of the fact that two components make up a StatefulWidget. This allows us to not only separate the type of interfaces logically but also physically in this case. The two State objects involved now live in two separate locations in the directory structure — in two separate files. Both are highlighted in the screenshot below.
Note, with this arrangement, now there’s no “stepping on each other's toes” when separate developers are assigned to these interfaces: One for android, the other for iOS for example. They’re working on separate files now! Each using separate libraries for their widgets:
The View Of Android And iOS
Looking now at the two classes, RandomWordsAndroid and RandomWordsApple, we can see what’s all involved with a quick glance at their import statements. We see the import statements in each that references the StatefulWidget counterpart, RandomWords. However, it’s the last import statement in each that may perk your interest.
The last import statement in each introduces the ‘Controller’ of this design pattern. Those familiar with MVC will know this is ‘the brains’ of the operation. It is common practice in this design pattern to have the interface (View) readily access a reference of the app’s logic in the form of a Controller. In this case, the Controller is responsible for the generating of names, and as it happens, in the screenshots below, the Controller’s variable, wordPair, is purple in color, and so you can readily see where it comes into play throughout the code. And so, the two screenshots represent the View in the app’s design pattern, and the variable, wordPair, represents the Controller in the app’s design pattern.
Below is the class, WordPair — the Controller in this design pattern. In this arrangement, the View knows the names of the functions of this class, WordPair. It can be said then the View knows how to ‘talk to’ the Controller. This is represented with an arrow going from the View to the Controller in the graphic below.
In the screenshot below, the Controller, WordPair, has two functions and two getters called by the two Views, RandomWordsAndroid and RandomWordsApple. Note, it’s not only ‘doing stuff’; this Controller is offering data to display like wordPair.current. However, this is not quite following the MVC design pattern. Where is the Model in this arrangement?
As it stands right now, the class, WordPair, serves as the Controller and as the Model. The data source, english_words.dart, is clearly retained inside the Controller. Thus, this class contains both the logic and the data source involved in this app. We could do better.
The ‘M’ in MVC
When there’s a Model included in the design, in many cases, the View (interface) talks to the Controller (logic), and the Controller talks to the Model (data). The View and the Model don’t communicate directly. In other words, for example, the View doesn’t know what the Controller’s property,
wordPair.current,actually retrieves, but nor should it need to. The Controller is ‘the bridge’ to the data. The View doesn’t need to know how to ‘talk to’ the Model (the data). Such an arrangement is then depicted as so:
Get More Generic
Note, the View and Controller could instead use more ‘generic’ terms to represent the API (Application Programming Interface) between them. In theory, you are then free to ‘switch out’ the Controller and or View with completely different code — as long and the publicly accessible functions, getters, etc. use the same names. See below. I’ve made some changes.
Controller By Name
The two screenshots below represent the Controller in the example app. The one on the right is the ‘latest’ version, and if you compare the two, you’ll see there are changes. The most pronounced change is that the data source, english_words.dart, has been moved out of this file and placed in another file named,
model.dart. The rest of the changes are the use of more generic terms.
The screenshot below introduces you to the Model for this example. You’ll see its contents and where the file is located. The Model ‘hides’ what sort of data it works with. The ‘english-words’ library file is now only accessed in the file,
model.dart. The rest of the app’s code need not know the source of the data. Further, with this arrangement, we could swap out that source with anything we like without affecting the rest of the app. Today, it’s English word pairs, tomorrow, its Chinese surnames or what have you. There’s this innate separation of roles now in the code allowing for easy change and scalability.
So there you have it. You now know how to present the appropriate interface. You now know Flutter offers static properties from its libraries to do just that, and I’ve offered an approach to implement them. Granted, I went on a tangent there with MVC, but you know how it is: Rules are meant to be broken.