jujujuju2
jujujuju2

Reputation: 31

Flutter app architecture using "modules" with provider package

I've been coding an app in Flutter since few weeks now and started wondering what the best architecture could be a few days ago.

A little bit of context first:

I've been experiencing different architecture approaches and managed to get one working that finally seems to suit me.

As I'll have multiple features, reusable at different places in the app, I want to split the code by features(or Modules) that can then be used independently in different screens. The folder architecture would be like this:

FlutterApp
|
|--> ios/
|--> android/
|--> lib/
      |
      |--> main.dart
      |--> screens/
      |       |
      |       |--> logged/
      |       |      |
      |       |      |--> profile.dart
      |       |      |--> settings.dart
      |       |      |--> ...
      |       |
      |       |--> notLogged/
      |       |      |
      |       |      |--> home.dart
      |       |      |--> loading.dart
      |       |      |--> ...
      |       
      |--> features/
              |
              |--> featureA/
              |       |
              |       |--> ui/
              |       |     |--> simpleUI.dart
              |       |     |--> complexUI.dart
              |       |--> provider/
              |       |     |-->featureAProvider.dart
              |       |--> models/
              |             |--> featureAModel1.dart
              |             |--> featureAModel2.dart
              |             |--> ...
              |
              |
              |--> featureB/
              |       |
              |       |--> ui/
              |       |     |--> simpleUI.dart
              |       |     |--> complexUI.dart
              |       |--> provider/
              |       |     |--> featureBProvider.dart
              |       |--> models/
              |             |--> featureBModel1.dart
              |             |--> featureBModel2.dart
              |             |--> ...
              |
             ...

Ideally each feature would follow these guidelines:

I've tried this approach with one feature (or 2 depends how you see it) of my app, which is the ability to record / listen to voice notes. I found it interesting because you can record at one place but listen to the recording at many places: just after the recording for instance or when a recording is sent to you as well.

Here is what I came up with:

In the code, it's a bit verbose but works as expected, for instance, in the screens, I can request the voiceRecorder feature like this:

class Screen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => VoiceRecorderNotifier(),
      child: Column(
        children: [
          AnUIWidget(),
          AnotherUIWidget(),
          ...,
          // The "dumb" feature UI widget from 'features/voiceRecorder/ui/simpleButton.dart' that can be overrided if you follow use the VoiceRecorderNotifier
          VoiceRecorderButtonSimple(), 
          ...,
        ]
      )
    );
  }
}

I can have the two features (voiceRecorder / voicePlayer) working together as well, like this:

class Screen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => VoiceRecorderNotifier(),
      child: Column(
        children: [
          AnUIWidget(),
          AnotherUIWidget(),
          ...,
          VoiceRecorderButtonSimple(),
          ...,
          // adding the dependent voicePlayer feature (using the VoiceRecorderNotifier data);
          Consumer<VoiceRecorderNotifier>(
            builder: (_, _voiceRecorderNotifier, __) {
              if (_voiceRecorderNotifier.audioFile != null) {
                // We do have audio file, so we put the VoicePlayer feature
                return ChangeNotifierProvider<VoicePlayerNotifier>(
                  create: (_) => VoicePlayerNotifier.withFile(_voiceRecorderNotifier.audioFile),
                  child: VoicePlayerButtonSimple(),
                );
              } else {
                // We don't have audio file, so no voicePlayer needed
                return AnotherUIWidget(); 
              }
            }
          ),
          ...,
          AnotherUIWidget(),
        ]
      )
    );
  }
}

It's a fresh test so I assume that there are drawbacks that I can't see now but I feel like there is few good points:

Text('hi')

but I can still "override" the UI for specific display of the feature;

The drawbacks I see:

Finally, here are the questions:

Upvotes: 3

Views: 3564

Answers (2)

Jess N
Jess N

Reputation: 11

Be aware of common functionality across features such as session/user information that also might come from a provider - but in general common functionality across features to prevent duplicated code.

Upvotes: 0

Omatt
Omatt

Reputation: 10519

Provider is a great tool that can help you access data all throughout the app. I don't seen any issues on how it's currently implemented on your app.

On the points that you're looking for like handling logic and updating UI, you may want to look into BloC pattern. With this, you can handle UI updates via Stream and the UI can be updated on StreamBuilder.

This sample demonstrates updating the UI on the Flutter app using BloC pattern. Here's the part where all logic can be handled. Timer is used to mock the waiting time for an HTTP response.

class Bloc {
  /// UI updates can be handled in Bloc
  final _repository = Repository();
  final _progressIndicator = StreamController<bool>.broadcast();
  final _updatedNumber = StreamController<String>.broadcast();

  /// StreamBuilder listens to [showProgress] to update UI to show/hide the LinearProgressBar
  Stream<bool> get showProgress => _progressIndicator.stream;

  /// StreamBuilder listens to [updatedNumber] to update UI 
  Stream<String> get updatedNumber => _updatedNumber.stream;

  updateShowProgress(bool showProgress) {
    _progressIndicator.sink.add(showProgress);
  }

  /// Updates the List<UserThreads> Stream
  fetchUpdatedNumber(String number) async {
    bloc.updateShowProgress(true); // Show ProgressBar

    /// Timer mocks an instance where we're waiting for
    /// a response from the HTTP request
    Timer(Duration(seconds: 2), () async {
      // delay for 4 seconds to display LinearProgressBar
      var updatedNumber = await _repository.fetchUpdatedNumber(number);
      _updatedNumber.sink.add(updatedNumber); // Update Stream
      bloc.updateShowProgress(false); // Hide ProgressBar
    });
  }

  dispose() {
    _updatedNumber.close();
  }

  disposeProgressIndicator() {
    _progressIndicator.close();
  }
}

/// this enables Bloc to be globally accessible
final bloc = Bloc();

/// Class where we can keep Repositories that can be accessed in Bloc class
class Repository {
  final provider = Provider();

  Future<String> fetchUpdatedNumber(String number) =>
      provider.updateNumber(number);
}

/// Class where all backend tasks can be handled
class Provider {
  Future<String> updateNumber(String number) async {
    /// HTTP requests can be done here
    return number;
  }
}

Here's our main app. Notice how we don't need to call setState() to refresh the UI anymore. UI updates are dependent on the StreamBuilder set on them. With each update on Stream with StreamController.broadcast.sink.add(Object), StreamBuilder gets rebuilt again to update the UI. StreamBuilder was also used to show/hide the LinearProgressBar.

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'BloC Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    bloc.fetchUpdatedNumber('${++_counter}');
    // setState(() {
    //   _counter++;
    // });
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<bool>(
        stream: bloc.showProgress,
        builder: (BuildContext context, AsyncSnapshot<bool> progressBarData) {
          /// To display/hide LinearProgressBar
          /// call bloc.updateShowProgress(bool)
          var showProgress = false;
          if (progressBarData.hasData && progressBarData.data != null)
            showProgress = progressBarData.data!;
          return Scaffold(
            appBar: AppBar(
              title: Text(widget.title),
              bottom: showProgress
                  ? PreferredSize(
                      preferredSize: Size(double.infinity, 4.0),
                      child: LinearProgressIndicator())
                  : null,
            ),
            body: StreamBuilder<String>(
                stream: bloc.updatedNumber,
                builder: (BuildContext context,
                    AsyncSnapshot<String> numberSnapshot) {
                  var number = '0';
                  if (numberSnapshot.hasData && numberSnapshot.data != null)
                    number = numberSnapshot.data!;
                  return Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        Text(
                          'You have pushed the button this many times:',
                        ),
                        Text(
                          '$number',
                          style: Theme.of(context).textTheme.headline4,
                        ),
                      ],
                    ),
                  );
                }),
            floatingActionButton: FloatingActionButton(
              onPressed: _incrementCounter,
              tooltip: 'Increment',
              child: Icon(Icons.add),
            ),
          );
        });
  }
}

Demo

Demo

Upvotes: 0

Related Questions