Building my first native mobile app in Flutter- first impressions and challenges
After spending many years building native Android applications in both Java and Kotlin, I decided to give Flutter a try and build an app from scratch that runs on both Android and iOS. Cross-platform development has been a hot topic for the past few years with many framework available such as React-native, Ionic, Xamarin, etc. that allow you to build mobile apps using known programming language like C# and JavaScripts.
Performance, ease-of learning, UI look and feel, and community support, are the most important factors to consider for choosing a platform to build your products. To me, Flutter meets all these factors with its Google’s Dart language. Honestly speaking, I didn’t have to learn Dart to build the app, perhaps due to my knowledge in Java. It was intuitive with Java-like syntax.
The app is a simple e-commerce store where users can add and manage products in addition to view other users’ products and make an order. I tried to include as many modules that almost every mobile application needs. It is called E-Book Shop.
The app
E-book Shop is a multi-pages mobile app built in Flutter for both Android and iOS platforms. The purpose of this project is to provide a starting point for developers who want to start building applications in Flutter for mobile and who prefer to learn programming languages by example.
This example contains important features that majority of apps require such as; handling Http requests and responses, run-time permissions, data persistence using SQLite, items lists and grids, checking for internet connectivity and listening to connection state changes, picking images from gallery, managing the state of the app, and how to use different widgets provided by Flutter which are platform-specific to help building beautiful applications in no time.
In this project, Firebase authentication, Realtime database, and Cloud storage are used for simplicity as these services are provided free of charge. If you have your own web server and APIs, you can make the necessary changes to what suits your requirements, however, the general approach of handling these APIs should be somehow similar.
Demo
Widgets
When I started with Flutter, it was really challenging because unlike native Android, there are no XML files where you can build your layout independently and see a preview of how the UI looks. Flutter is all about widgets. At the beginning, it can be tricky to know which Widgets are provided by the platform, which are really available, and which I need to build myself. However, Flutter widgets Catalog web page got me covered, it is the place to understand what kind of widgets you can use and how to use them and wrap them in each other to come up with the responsive layout expected. The website provides great documentation with examples on how to build beautiful UIs in Flutter. In addition, Flutter studio is a great tool to understand how Flutter widgets work and what attributes are parameters are provided for each. It is worth mentioning that for any IDE you choose to build for Flutter, make sure you install Flutter and Dart plugins which will help showing possible attributes each widget has (Basic Completion) such as by clicking ctrl+space in Android Studio in addition to many other features that simplifies the development process and make it faster.
Threading: futures, async, await
Dart is a single-threaded language which means an app written in Flutter can only run one thing at a time. However, it supports asynchronous operations where the app can complete a task while waiting for another that is running in the background to finish, e.g. you can navigate the app while waiting for products to be fetched over the network without freezing the UI. This is achieved in Flutter using futures, async, and await. The pattern is simple, get me the results (future) when (await) the data being fetched from network(async) is returned.
It is written as the following snippet shows;
Future _authenticate({@required email, @required password, @required url}) async { try {
//Refer to firebase auth api for documentation
final response = await http.post(url,body: json.encode({
‘email’: email, ‘password’: password,‘returnSecureToken’: true
})).catchError((error) {
throw error;
});
.
.
.
}
State Management
Managing the state of your application is a very important process in the app’s lifecycle to ensure performance and the data availability when needed. State Management is the most tricky part in development for Flutter — at least for your first app- . It is important to understand how that works especially when your app gets bigger and more complicated. In Flutter, there are 2 types of state;
- Ephemeral state: which is sometimes called UI or Local state. It can be implemented using State and setState(). This state is often local to a single widget and needs to declare a StatefulWidget which is a widget with mutable state, unlike StatelessWidget. For example, the selected tab in BottomNavigationBar or loading state to determine whether to show a loading spinner or not.
import ‘package:flutter/material.dart’;class HomeScreen extends StatefulWidget {
.
.
. bottomNavigationBar: BottomNavigationBar(
body: _tabs[_selectedPageIndex][‘tab’],
onTap: _selectPage,
.
.
. void _selectPage(int index) {
setState(() {
_selectedPageIndex = index;
});
}
}
So when the bottom navigation is tapped, we change the index of the selected page and call setState() to rebuild the widget and update what tab the “body:” should now display. For more information about that, please look at this article.
- App state: this state is shared across many parts of the app and what needs to be kept between different sessions. For example, deciding if a user is logged in to keep allowing him to navigate the app or not and maybe redirect him to login page. This state can be managed through using Providers for example.
The type of the state can be managed simply by deciding who needs the data; if it is just a single widget like showing a spinner, then it is the local state, but if the data is needed in some or most widgets, then it is needed to be the App state.
Widgets in Flutter are immutable, they get replaced instead of being changed. To make working with widgets easier where in Flutter, everything is a widget, I used the Provider pattern.
Providers
Providers are a mixture between Dependency Injection (DI) and state management. It is used to manage the app’s state by offering the following;
- Change Notifier: it simply provides change notifications to its listeners. In other words, you can subscribe to changes and make the necessary changes upon receiving a change notification after notifyListeners() is called. For example, when the user clicks on star icon to favorite/unfavorite an item, we change the value of isStarred and notify a Consumer of the change to rebuild a widget and hence a piece of UI or the entire page.
class ProductProvider with ChangeNotifier { void _setStarredValue(bool isStarred){
this.isStarred = isStarred;
//notify that the value has changed
notifyListeners();
} }
- Consumer: it allows using the provided model (provider) to a widget, in our example, the isStarred value to change the star icon in the page. Consumer has a build function that is called every time the ChangeNotifier changes upon NotifyListeners call. The builder has 3 arguments; context that is provided in every builder method in Flutter, ChangeNotifier, which is what we are looking for, this data is used to determine how the ui should look like, and the third argument is a child.
It is best practise to place the consumer as deep in the widget tree as possible only to wrap what needs to be rebuilt to avoid rebuilding a large portion of the UI which can impact performance.
In case the data is not required to change the UI but it is required to access it, we use Provider.of
final authProvider = Provider.of<AuthProvider>(context, listen: false);
- ChangeNotifierProvider: it is the widget that provides an instance of ChangeNotifier. It is placed above the widget that needs to access it.
ChangeNotifierProvider(create: (_) => AuthProvider()),
You can create one or multiple providers based on the nature and the size of your app. In this project, I created providers for Cart, Products, Orders, and Shop items, in addition to an Auth provider to handle login, signup, and handling user’s token session in the entire app. All these providers should be registered at main.dart widget through using MultiProvider and providers for multiple providers.
Data Persistence
Flutter supports SQLite databases to persist data on local devices. Databases provide faster queries, inserts and updates compared to other data persistence solutions.
In order to use the database, sqflite and path packages are used in this project. Before data can be read and written to the database, we have to open a connection to the database by defining the path using the packages and then open the database.
In this app, user_products table was created where each product has an id, name, description, image url, and price.
The database provides a set of functions to insert, delete, update, and query to read and write data.
For simple data storage, I used a shared_preferences package plugin that wraps SharedPreferences on Android and NSUserDefaults on iOS.
// save to shared preferences
SharedPreferences.getInstance().then((prefs){
prefs.setBool(“isLight”, isLight).then((val) {
});
});// Retrieve data
final prefs = await SharedPreferences.getInstance();
ThemeData themeData;
bool isLight = prefs.getBool(‘isLight’) == null ? true : prefs.getBool(‘isLight’);
Styles and Themes
Flutter has done a great job in handling the app’s theme. You can set the theme in a few lines of code. I created style file which includes 2 different ThemeData, for light and dark themes. You can switch between them based on users preferences decided in settings page. Here is an example;
And then it can be specified to reflect the changes in the UI.
MaterialApp(
.
.
theme: [YOUR-THEME],
.
.
)
If you want to set different widgets for Android and iOS, you can simply check for the current platform and render the widget accordingly;
Platform.isAndroid
? new CircularProgressIndicator()
: new CupertinoActivityIndicator()
There are a few other exmples in the project like AlertDialog widget and Toolbar action buttons to reflect the right widget based on the platform.
If you want to view the source code and try the project yourself, you can clone or download it at Github. View Usage section to see how to setup the project and run it on your Android or iOS device.
Conclusion
In general, using Flutter was a fun experience. Definitely, it is a complete platform with lots of components that allow you to customize your app for different platforms and build applications very quickly with the expected performance. Dart is a powerful programming language that is easy to learn and use. The next step is to build more complicated applications with Websocket and VoIP capabilities to test Flutter’s performance in such cases.