Search code examples
androidflutterflutter-blocflutter-cubit

Issues initializing a Flutter cubit to automatically call a data loading function


I'm struggling to understand how to initialize a Flutter cubit such that it can load data automatically when it's first created.

I saw 2 general recommendations during research, people say you can either call an initialization method in the cubit constructor or you can call it in the BlocProvider where the cubit is created using .. notation.

I put together an example screen (code below) based on research but the cubit does not print the initialize message in the initialize method automatically as I would expect.

It does however, print the following sequence of log messages only after I click the FAB (just a simple call to a separate test method in the cubit).

[log] TestScreen1Cubit constructor

[log] TestScreen1Cubit.initialize() - Initialized from [TestScreen1Cubit Constructor]

[log] TestScreen1Cubit.initialize() - Initialized from [TestScreen1 BlocProvider]

[log] TestScreen1Cubit.printHello() - Hello

Any ideas or suggestions about why the TestScreen1Cubit's initialize method isn't called automatically?

Here is my project structure:

lib/
    app.dart
    main.dart
    test_screen_1/
                  cubit/
                        test_screen1_cubit.dart
                        test_screen1_state.dart
                  repository/
                             test_screen_1_repository.dart
                  data_provider/
                                test_screen_1_data_provider.dart
                  ui/
                     test_screen_1.dart
                     test_view_1.dart
    utils/
          MyDebuggingBlocObserver.dart

Here are my test project files:

Note that I have a repository and data_provider here which would be utilized during the cubit initialization but they aren't actually used in this current example because the cubit initialize method isn't called automatically for some reason

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import 'app.dart';
import 'utils/MyDebuggingBlocObserver.dart';

Future<void> main() async {

  BlocOverrides.runZoned(
    () => runApp(
      App(),
    ),
    blocObserver: MyDebuggingBlocObserver(),
  );
}

app.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hello_new_project/test_screen_1/data_provider/test_screen_1_data_provider.dart';
import 'package:hello_new_project/test_screen_1/repository/test_screen_1_repository.dart';
import 'package:hello_new_project/test_screen_1/ui/test_screen_1.dart';

class App extends MaterialApp {
  App({Key? key})
      : super(
          key: key,
          routes: {
            TestScreen1.routeName: (context) => RepositoryProvider(
                  create: (context) => TestScreen1Repository(
                    TestScreen1DataProvider(),
                  ),
                  child: const TestScreen1(),
                ),
          },
          initialRoute: TestScreen1.routeName,
        );
}

test_screen_1.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hello_new_project/test_screen_1/cubit/test_screen1_cubit.dart';
import 'package:hello_new_project/test_screen_1/repository/test_screen_1_repository.dart';

import 'test_view_1.dart';

class TestScreen1 extends StatelessWidget {
  static const String routeName = "/TestScreen1";

  const TestScreen1({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => TestScreen1Cubit(
          context.read<TestScreen1Repository>()
      )..initialize("TestScreen1 BlocProvider"), //********** Suggestion #2 ********** //
      child: const TestView1(),
    );
  }
}

test_view_1.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'dart:developer' as developer;

import 'package:hello_new_project/test_screen_1/cubit/test_screen1_cubit.dart';

class TestView1 extends StatelessWidget {
  const TestView1({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("TestView1"),
      ),
      body: _buildBodyContent(context),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () {
          context.read<TestScreen1Cubit>().printHello();
        },
      ),
    );
  }

  //region Build Methods

  Widget _buildBodyContent(BuildContext context) {
    return const SafeArea(
      child: Padding(
          padding: EdgeInsets.all(8.0),
          child: Text("Hello"),
      ),
    );
  }



  //endregion Build Methods

}

test_screen1_cubit.dart

import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import '../repository/test_screen_1_repository.dart';
import 'dart:developer' as developer;

part 'test_screen1_state.dart';

class TestScreen1Cubit extends Cubit<TestScreen1State> {
//region Class vars

  final TestScreen1Repository _testScreen1Repository;

//endregion Class vars

//region Constructor

  TestScreen1Cubit(this._testScreen1Repository) : super(TestScreen1Initial()) {
    developer.log("TestScreen1Cubit constructor");
    initialize("TestScreen1Cubit Constructor"); // ********** Suggestion #1 ********** //
  }

//endregion Constructor

//region Public methods

  /// Prints a simple log message with param [fromWhere] indicating where this
  /// method was called from
  void initialize(String fromWhere) {
    developer
        .log("TestScreen1Cubit.initialize() - Initialized from [$fromWhere]");
  }

  /// Simple public method to print hello, called by the FAB for testing
  void printHello() {
    developer.log("TestScreen1Cubit.printHello() - Hello");
  }

//endregion Public methods

//region Private methods

//endregion Private methods
}

test_screen1_state.dart

part of 'test_screen1_cubit.dart';

@immutable
abstract class TestScreen1State extends Equatable {
  const TestScreen1State();
}

class TestScreen1Initial extends TestScreen1State {
  @override
  List<Object> get props => [];
}

class TestScreen1Loading extends TestScreen1State {
  @override
  List<Object> get props => [];
}

class TestScreen1Loaded extends TestScreen1State {
  @override
  List<Object> get props => [];
}

pubspec.yaml

name: hello_new_project
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 1.0.0+1

environment:
  sdk: '>=2.19.2 <3.0.0'


dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2

  flutter_bloc: ^8.1.2
  equatable: ^2.0.5
  fluttertoast: ^8.2.1

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0

flutter:

  uses-material-design: true

MyDebuggingBlocObserver.dart

import 'package:flutter_bloc/flutter_bloc.dart';
import 'dart:developer' as developer;

class MyDebuggingBlocObserver extends BlocObserver {
  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    developer.log('${bloc.runtimeType} $change');
  }
}

I have tried to implement both of the suggestions I saw (see the lines with ********** Suggestion <1 or 2> **********) but the initialize method still does not get called automatically. It looks like it only gets called after the call to the FAB test method is made instead of automatically as I would expect. The log from the FAB test method still appears after the constructor and initialize logs as expected but does the cubit not get created until a method call to it is made?


Solution

  • this is normal behavior. the reason it doesn't match your expectations is that BlocProvider creating TestScreen1Cubit in a lazy way - just when you start to use it and not before. that's why you see all the logs only after pressing FAB.

    in order for TestScreen1Cubit to be created immediately, you can use BlocBuilder in your code where you need it.

    for example, in TestView1:

      Widget _buildBodyContent(BuildContext context) {
        return SafeArea(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child:  BlocBuilder<TestScreen1Cubit, TestScreen1State>(
              builder: (context, state) {
                // you can use your state here
                return const Text("Hello");
              }
            ),
          ),
        );
      }