Post Category
→HabitChallenge

Lets talk redux for a brief moment. As you may know there are three main ingredients in redux:

action,

reducer,

store.

From the first look this kinda makes sense. You have your store with is actually your app current state. Reducers are pure functions, as such they are disconnected form everything, just take current state, and action to produce new state. Finally, actions, those are like messengers, caring around data for a reducer (transformer).

Since all actions are (or at least should be) easily serializable, you get this amazing feature of time traveling within your app.

On a down side. It makes it bit harder to reason about the code. Since actions and reducers may be defined in separate files. Also, sometimes the same action may be used with different reducer in potentially many different places. A lot things can happen since you have a freedom to mix-and-match actions and reducer!

Can we do better?

I think we can! Some time ago I have found async_redux library and since then cannot live without it! In HabitChallenge app I went from Provider to rx_command to async_redux and I do not plan to move again!

What is all the fuss with your own reducer? In async_redux, each action is a subclass of ReduxAction, that have an abstract reduce() method. So each action, defines its own parameters and have bundled in reducer. Then you are dispatching it as normal redux action.

This way action and reducer are always together, in one file, even more, in one class!

Inside of ReduxAction you have access to whole state object. So you can use it for computation (if needed) or to get additional data for your REST endpoints.

reduce() method of course returns whole new state, so you can modify different parts of your app/state at one go, in a single action.

ReduxAction have useful before() and after() methods. In case you need to do something before and/or after the action. Like show and hide progress indicator somewhere in the UI.

If you need to process errors there is wrapError() method. And global error listener!

Whats more, each of before(), reduce() and after() can be asynchronous! Can also dispatch other actions using dispatch() call!

Instead of middleware, there is an ActionObserver interface so that you can do some fancy side effects! If you want to.

Also StoreConnector widget forces you to use View pattern for all of your widgets! This is also the place where your UI and state meets! Where you extract data from state to show to the user and where you initially dispatch actions.

If you care about your app performance, you should definitely give a try for async_redux. Since BaseModel class (one that is use to bind your UI to the store) lets you define list of properties that when changed will re-render the widget. Otherwise widget will stay as it is. This is so awesome, because you can use values that were computed base on current state, no need to include all the parts from your store!

Have I mentioned about test-ability? There is a dedicated StoreTester class, that lets you easily define initial state, then execute an action(s) and verify resulting state. It also lets you check all the actions that were fired (if any) and also verity order and parameters of actions. Finally you can of course verify the state it self.

The key takeaway from all of this, is that you should at least scan through async_redux README.md file and give it a try in a free moment.

Probably APK (or app bundle) and IPA size is not something that is causing you sleepless nights. At least it isn’t for me.

We know that there are some tradeoffs when going cross-platform route. Most of people will name performance or look-and-feel as main concerns. Maybe some will mention the deliverable size. This is what I will tackle in this blog post. The size of your app for the end user.

Of course, going the cross-platform route we accept that our deliverable would be bigger. The main reason is that platform does not provide the runtime for our out app. We always need to ship everything to start and run our app.

Why should you care about your download size? In most cases there are two, three, or thirty other apps that will do exactly the same thing as yours. If your potential user need to wait one more minute to install yours app, this is potential one minute when he/she can give a try for a competitor app. This is also more time when something may go wrong, more bytes to transfer means higher probably that something will go wrong with the internet connection etc.

Also smaller app size may be a factor for the Play/AppStore listing. As Apple/Google needs to push all of that data to the user and they need to pay for the upload 😀 (just kidding ;)). But anyway, at least Play Console shows how your APK size compares to the app category median.

Some basic things you can find in this blog post, I will cover all the rest ;). Would recommend you go over this blog first, configure progurad etc. and get back here for more!

R8

This is Android specific. It is a successor to proguard, that can also remove not used resources. You can enable it by adding android.enableR8=true to android/gradle.properties (although it may be already enabled). One thing to note is that it may remove not used resources, like eg. your notification icon, please follow instructions from flutter_local_notifications in order to keep your notification icon.

Fonts

If your app uses custom fonts, I would recommend finding alternatives in Google Fonts, and then adding google_fonts to your dependencies. You may wonder how additional dependency may reduce your APK size? Well, first of all you may remove all other fonts from your project. Secondly google_fonts will dynamically download font when it is used and it will download it in a specific style and language, so that you don’t need to ship all of the variation with your project. Yes, this is another tradeoff, most probably fonts will be downloaded on app initial start… but it is less bytes to push when app is installed.

Icons

You probably use a few icons either from Icons or CupertinoIcons class. Normally this means that all of the icons are embedded into your Flutter bundle. But since flutter 1.17, you can add --tree-shake-icons option to flutter build command, that would remove all of the not used icons from the bundle. Few more bytes shaved!

Split Debug Info

You probably don’t want to include Dart debug information in your production release. --split-debug-info is another option you can add to flutter build command to store debug information separately. This option takes additional argument that is a directory where debug information should be stored.

In case of my HabitChallenge app, I have created a git submodule, where I automatically store, and tag, debug information after the release.

Then when you need to add debug information to your stacktrace, you can use flutter symbolize command and point to appropriate version of debug-info.

Putting All Together

In my case, I have created bash scripts that handle the release process (let me know if you are interested to know how I do releases for HabitChallenge).

Android

For android things are fairly easy, since you can manually upload APK(s) or App Bundle to Play Console (or point fastline to do it for you automatically).

What is going on here? First of all I’m getting current app version from pubspec.yaml. Then I am running flutter build command with all of the size optimization flags. The debug-info output is stored in a path that contains app version and platform. Finally I am going into debug-info directory and committing and pushing all the changes.

iOS

The thins are a bit trickier here. Since we need to use xcode with its own build system to deploy our app to AppStore.

Start the xcode, open your Flutter app, then select Runner root node, then Runner target, next Build Phases, and finally expand Run Script node. Here we need to do some changes…

No worries, I won’t force you to retype all this in, here is a plain text version of the release script:

This time, we first discover if build is run in Release mode, then again we get the app version from pubspec.yaml to store debug information in appropriate directory. Then we export environment variables that are read by the build script. And finally run the Flutter’s xcode build script!

Summary

How much can you shave? Well that depends on how much fonts and icons your app is using 😉 Regarding the debug-info, in case of HabitChallenge it saved about 900KB (from 26.3 to 25.4MB) of App Bundle. According to Play Console app downloadable size is from 9.50 to 10.3MB.

Do you think it is worth it? Will you enable those optimizations in your build?

As people, we are always craving novelty, things that we are familiar with makes us feel cozy but also do not attract as as much they were right on the beginning. This why your app should constantly engage the user. How to achieve this? You can for example have multiple messages for same operations.

In HabitChallenge there are two features that have multiple localized messages:

local notifications,

streak detection.

Each notification will have different message in form of a question, asking if you already did your habit.

For streak detection it is a bit more complex. Each streak message depends on the streak length and has not only random message, but also header and text that you can share with your friends.

Nevertheless both require a way of providing pseudo random message in one of the supported languages. Plus some of the languages have more options than others (yes, Polish users are less likely to see the same message in row).

In react-native times, this was easy! Since i18n-js relays on JSON configuration file, I was able to get extract translated raw list, then it was easy to check its size and finally generate random number within given range.

It is not as easy in Flutter. Mainly because initially I decided to go with the standard localization library intl, and learned half way through that I cannot use the same approach as I had in react-native with it.

You may ask why? First of all intl does not use JSON to store translations, but specialized ARB files. Main advantage of ARB is that it is supported by software that is used by professional translators.

The downside, is that only data structure that it supports is a map and you can’t access it directly. This makes things a bit harder… but there is more!

intl, does not use ARB files directly, they are only used as a transportation layer. With intl you can extract all messages from your app into ARB file, give this file to translation team (if you are lucky to have one). Then get back one ARB file per language and import them back to your app as dart source files.

Yes, there is some black voodoo magic with code generation in between. When you get ARB files, you need to run them through intl command line tools, to generate dart file with translation.

With code generation there are also some limitations. You can use placeholders, but you can’t call any methods on them. Instead of passing an object and extracting its properties, you need to pass each property separately as a parameter 😐

As I mentioned before, intl have support for maps… not exactly map data structure, but at least we can think about it as a map. I am talking here about Intl.select() where you can select one option base on provided key. Lets try to use it:

If we now run ARB generator it will finish without any complains. But on a closer of ARB file there is only one translation assigned to a ‘key’. Simply because ARB generator didn’t get that in this context ‘key‘ is a variable, crap… We can easily fix this, just replace key: with ‘$key’: that should do!

No it do not 🙁 selectors must match regex [a-zA-Z][a-zA-Z0-9_-]*. This means selector must start from a letter and cannot have $ sign. This means we cannot use string interpolation here… crap… But there is another workaround for this!

Instead of doing interpolation in Intl.select() parameters, we can do it one level above:

Now intl will parse our code without any complaints and generate valid ARB and dart files! Yeah! Now we can get translation based on a number!

Side note: I have chosen ‘m_$key’ format on purpose. Internally ARB will keep maps in a single long string where each option is formatted like so:m_1{First message}m_2{Second message}m_3{Third message}. Since I personally edit all of the translation files manually in a plain text, it is easier to eyeball separators like ‘m_1’. You can choose different approach.

That was easy! We simply take another random number, add one, get translation from int, if it is empty fallback to ‘m_1’ as our key otherwise, return value that we got from intl.

Now, the last part. We need to set upper boundary for random generator. If we do not do that, then most of time we will get translation that is set under ‘m_1’ key.

There are many ways how to solve this. One is to store this in code in some form of an object, map or constant. I have decided to keep it together with translation and store it in file. This mean, we have one more Intl.message() that will return us a number. Here is complete code:

Here is a recording of my FlutterLDN talk about ReactNative and Flutter.

First commit for the HabitChallenge app was created on 27th of July 2017. Back then there were no other choices than to go with react-native and JavaScript.

Since then I did 3220 commits and 244 releases. App was downloaded over 140k times on Google Play and AppStore.

I am considering this a huge success, taking into account that HabitChallenge is my first mobile app and I did virtually no marketing for it!

Long story short on 7th of August 2019 I have decided to start from scratch with Flutter taking all my learning from react-native and writing every single widget and feature once again.

After 531 commits (yes, this is the actual number of commits from git history) on 24th of December, HabitChallenge 5.0 was released on Google Play!

In the talk I am comparing side by side the same widgets implemented in react-native and Flutter. This is a unique real world comparison of two technologies that lets you achieve the same effect… but with different cost.

Being one-man-army working on an app has some advantages. There is no deliberation about code formatting, libraries or frameworks used in the project. This is why migrating from react-native to Flutter was an easy decision, I just didn’t need to convince anyone apart from myself ;).

Of course all of those pros are also the cons. There is one that could prevent me from making bad decisions, checking in a bad code or braking old features while implementing new one. There is no one to deliberate about algorithmic, implementation and architectural choices in code review… you get the point?

There are some ways how some of those down sides may be overcome. For example writing unit and functional/integration tests is one of them. Code formatter and analyzer are another two.

Thankfully Flutter comes with all of the bells and whistles. There is flutter format, to automatically format your code, and flutter analyze to analyze code and enforce quality. Even more, both tools comes with awesome pre-build configurations that are used by Flutter team.

Unfortunately there is nothing that would automatically write test for you ;). But fortunately there are high quality and performant test frameworks that lets test your code on:

unit,

widget (functionality),

widget (presentation),

integration

levels.

One thing is to have the tools, but another is to use them… When you are part of a bigger team, someone will configure a build and CI/CD system for you, that probably will be running in the cloud. Or you can use something like CodeMagic… But in my situation I try to limit external services that I use with HabitChallenge.

One thing that I loved in my react-native times, was husky project. That lets you easily define and manage git hooks, especially pre-commit hook. This way I was able to run google typescript and unit tests before each commit.

Back in August 2019, there was nothing that may be compared to husky in dart/flutter ecosystem. So I decided to go with a general solution, the pre-commit project. Lets see how we can leverage pre-commit to ensure our code is properly formatted, doesn’t break any analyzer rules and do not break existing unit tests.

First of all we need to install pre-commit in our system, this is as simple as: $ curl https://pre-commit.com/install-local.py | python –or $ brew install pre-commit

Secondly, we need to create a file called .pre-commit-config.yaml in our repository root with following content:

Finally run pre-commit install in the repository root and we are done!

OK, but what it is actually doing? Whenever you modify any dart file and decide to commit your changes to git repository, pre-commit will automatically run flutter format, flutter analyze and flutter test for you. If any of those command fails you will not be able to commit your changes. First you need to fix what have you broke then call git commit again. The good part is that when you modify any other file none of those commands are run!

This approach has two downsides. First of all, flutter format will automatically fix any formatting issues for you… but because of limitation in pre-commit you can’t modify files during pre-commit hook. This means that after files are properly formatted you need to run git commit again. Secondly, depending on your hardware, amount of code and number of unit tests it can take considerable amount of time to execute all those checks. You have been warned!