Fedi — Flutter open-source social network client. Part 1. Architecture.

Yevhenii Zapletin
8 min readJul 21, 2021

--

Architecture and Patterns used in Flutter open-source social network client: Provider, Repository, StreamBuilder, BLoCs, Dependency Injection, and more.

Fedi is an open-source client for Pleroma and Mastodon social networks written using Flutter available on iOS(Beta) and Android(Beta)

Pleroma and Mastodon are parts of Fediverse(decentralized social network). Nobody owns Fediverse. Anybody can run their own server instance and use it to communicate with other people.

So Fedi is an open-source mobile client for social networks and has features similar to Twitter.

Series:

Series content:

  • Project structure & patterns;
  • List of used 3rd parties libraries;
  • Build tricks;
  • Useful tools for Flutter development.

There are a lot of code Gists, links to code in the repository, and links to other articles so you will see how things actually work.

There are a lot of issues and features in Fedi backlog. And some code pieces require refactoring and improvements. So Fedi is not an ideal example of the Flutter app, it is a real-world project which actually works. So leaving a comment and issue or even contributing is welcome.

I think this articles will be useful for:

  • developers who have experience with other technologies(especially iOS & Android) and want to try Flutter. You will see how things you know work in Flutter
  • developers who have only started learning to program and want to explore how middle-size projects work
  • developers who want to join an open-source project
  • people who are interested in Fediverse(Pleroma, Mastodon etc) and want to help develop Fedi

In this part

Dart/Flutter community devs have different backgrounds. So you can find a lot of different approaches which actually work but look very different.

There is no silver bullet that you should use. I suggest explore them all and choose which works better for you. Some of the useful links:

I used to be Android Developer so almost all ideas come from there.

Overview:

  • Dependency Injection via Provider
  • Using BLoCs for business logic and view state
  • Don’t use StatefullWidgets. Updating UI via StreamBuilders. Using Bloc classes injected by Provider
  • Having own implementation of BLoC(don’t use bloc package)
  • Composition over inheritance
  • No Singletons. Injecting global things on app-level
  • Re-run app if need
  • Interfaces. A lot of Interfaces
  • Divide Widgets into smallest Widgets if possible with const constructor
  • Using Repository pattern to achieved data consistency
  • Immutable Data classes

Note: There are a lot of TODO and several not yet refactored code pieces in Fedi right now, so not all things follow described approach.

· Dependency injection via Provider
AppContextScope & UserContextScope
DisposableProvider & DisposableProxyProvider for BLoCs
· Data classes
Immutable
equals() & hashcode & toString()
Provide
· No singletons
· BLoCs
Is BLoC a ViewModel?
Async init
· Isolates
· Re-run app
Splash
Login screen
Logged screen
Several accounts
· Fedi doesn’t use StatefullWidget
· Streams
Map streams
Twice build with StreamBuilder
· Data consistency & Offline mode
· Prefer Interfaces
· Composition over inheritance
· Local preferences
Own BLoC for each key
Objects scheme version
· Performance
· Testing

Dependency injection via Provider

Provider gives a possibility to inject any classes in widget tree and fetch it later from BuildContext

AppContextScope & UserContextScope

Fedi defines several context scopes:

  • AppContextScope — contains global things, like database instance
  • UserContextScope — contains user-related things. Like REST wrapper with auth token

Usage in Fedi(should be refactored, perhaps with Service Locator pattern):

DisposableProvider & DisposableProxyProvider for BLoCs

Fedi also uses easy_dispose for easier resources disposal of provided blocs

Data classes

Immutable

All fields in Data classes should be final. If you want to update an object you should create a new object or implement copyWith() method (can be generated with Dart Data Class plugin).

Immutable classes guarantee that everybody will know about updates via BehaviourSubject or StreamController logic.

equals() & hashcode & toString()

In addition to copyWith() don’t forget to override hashcode and equals methods on all data classes otherwise objects comparison will not be efficient

Overriding toString() is optional and should be used only for more informative logging messages

You can generate all things via Code->Generate option in Android Studio

Provide

Data classes are provided via Provider.value or StreamProvider. Since Fedi prefers const Widget constructors for performance it is not possible to pass data classes to widgets as fields.

No singletons

Since we can provide something globally one-time on AppContextScope we don’t need singletons(with static instance).

Benefits:

  • Better dependencies between features;
  • Easy to replace implementation;
  • Easy to test & mock.

BLoCs

Example of BLoCs used in Fedi

Is BLoC a ViewModel?

Some BLoCs in Fedi are actually ViewModels(naming should be refactored):

  • Have UI controllers as field: TextEditingController, TabController, ScrollController, etc;
  • can have links to other BLoC/ViewModel objects;
  • may not have any logic, just listen to UI controllers and call other BLoC methods.

Async init

Sometimes, you need to execute something async Future on object init.

For this case Fedi uses special AsyncInit classes and UI which handle object state:

Isolates

Fedi uses compute for:

Actually manual(with some specialized package which may use compute() inside) calling compute() is not necessary in almost all cases.

Don’t forget that spawning isolate has its own overhead and in some cases compute() may slow down your app. Why, When, and How you should use isolates ?— Why should you use isolates in Flutter?

Re-run app

Fedi calls runApp() several times. It is useful in some cases.

Splash

Show the first Splash page as soon as possible before all AppScope context is initialized.

Login screen

Check AppScrope and if the user has not logged in Fedi will runApp() with LoginScreen home page

Logged screen

Check AppScrope and if the user has already logged in Fedi will runApp() with SplashScreen home page to initialize UserScope and move to LoggedScreen once initialization is finished.

Several accounts

You can login to several accounts with Fedi at the same time. runApp() is used to switch between accounts.

Fedi doesn’t use StatefullWidget

Fedi uses StatefullWidget only in 2 cases:

  • not refactored yet classes
  • to use TickerProviderStateMixin

All logic is moved to BLoC(BLoCs in Fedi can be named as ViewModel in some cases):

  • TextEditingController, TabController, ScrollController etc
  • Initial actions and events subscription in BLOC constructor instead of createState method
  • Usually, BLoC for StatelessWidget is created and provided just by the above widget in the hierarchy, so it has the same lifecycle as StatefulWidget. BLoC may be provided on Page or even AppContextScope, so BLoC will maintain state even if the widget is destroyed/recreated.

Streams

Streams are used everywhere in Fedi:

  • one BLoC listens to another BLoC/Servies streams events;
  • UI updates only by StreamBuilder on streams;
  • Moor SQLite ORM provides API to watch(have Stream) for any selected statements;
  • much more

Map streams

Fedi often converts Parent Stream type to Child Stream type. It is useful for encapsulation when only Child type is actually needed and Parent is unnecessary

Distinct streams

Stream has a useful distinct() method to avoid emitting value if previous value is the same (don’t forget to override equals & hashcode)

It is useful for performance: you can avoid widgets rebuild via StreamBuilder if it is not necessary

Twice build with StreamBuilder

Fedi uses StreamBuilder a lot and one thing looks weird to me. Build method is called twice on first init. A possible workaround is to Provide.value stream value with const Widget to avoid nested widgets twice rebuilding.

Useful links

Data consistency & Offline mode

Fedi has an offline mode:

  • Caching almost all network data in SQLite database;
  • Some data is cached only in memory, but it is acceptable only for small collections to avoid large memory usage;
  • Offline mode makes it possible to get access to cached data without a network;
  • It improves UX because the user shouldn’t wait for the network. Fedi displays cached data ASAP and updates from the network in the background;
  • UI always displays data from Repository classes which holds cached and network data. It is important to have one place where all data is consistent and synchronized. In Fedi all data is synchronized by Moor SQLite ORM. Repository classes just update the database and UI always display the latest available data from the database.

Prefer Interfaces

Prefer interfaces for Bussines Logic and Services

  • Simple append I to implementation class name. AccountFollowerAccountCachedListBloc implementation and IAccountFollowerAccountCachedListBloc interface
  • Code readability: you can see a small list of public methods/fields in the interface file instead of exploring long files with implementations
  • It is useful to implement extensions for interfaces not for implementations
  • It is useful to extend several interfaces in one child to separate logic
  • It is useful to create tests and mocks

Composition over inheritance

During Fedi development some inheritance became too complex and hard to maintain. Inheritance works cool for interfaces but not for complex implementations.

Useful article: Prefer Composition Over Inheritance

Note: Mixins is composition because you can compose classes with several mixins

Local preferences

Fedi stores simple data (without complex relationships) in simple local preference storage by using hive package

For example UI preferences, Language, appOpenedCount to showRateMe dialog etc

Own BLoC for each key

Since fedi uses a feature-based folder structure, having one file with methods like getUISettings , getCacgeSettings etc is not good.

So instead of getUISettings method Fedi has UiSettingsLocalPreferencesBloc class.

Usage in Fedi: local_preference_bloc_impl.dart

Objects scheme version

In addition to owning a class for every preference, I also recommend to append schema version to the key.

user.settings.UI -> user.settings.UI.1

This will help in the future when you will change UI settings object structure and want to migrate data from an old version.

Performance

How Fedi improves performance:

  • Database: batch transactions & indexes
  • Widget with const constructor
  • Divides big widgets into small widgets
  • calls distinct() on streams for StreamBuilder
  • initializes BLoCs asynchronously and displays loading UI
  • doesn’t use blur and transparency if possible
  • uses vector icons font instead of simple icon files
  • always overrides equals & hashcode for data classes
  • uses isolates for json parsing & comparing big collections

Don’t optimize too early. Before making any improvements profile your app (Run on Profile mode on real device, not emulator) and check the performance overview. You should have a stable frame chart near 60 fps. In most cases, you will see that existing code works well without optimizations.

Useful article: Performance best practices

Testing

Fedi is covered with unit-tests(unfortunately it is not 100% coverage yet):

  • makes app more stable
  • helps write cleaner code. It is very hard to write tests for spaghetti code
  • helps to be confident during refactoring, you can always check that nothing has been broken
  • mockito — cool package to mock dependencies and test things independently. It is much easier to mock interfaces

Fedi doesn’t have UI, integration and functional tests:

  • Flutter UI tests don’t look too helpful for me. Testing complex things requires a lot of time. It is good to have but too time costly.
  • Integration and functional tests look much more effective for me. Especially for Fedi, which can connect to different Mastodon & Pleroma servers with different versions.

Final Words

Feel free to comment and fill issue if you don’t agree with something or have suggestions.

Next partPart 2. Code.

Start using Pleroma and Mastodon with Fedi if you still not in Fediverse: iOS(Beta) and Android(Beta). Any feedback is welcome.

If you are interested in Fedi and want to help to develop it you can start from Readme.

--

--