gbtb16 / kiwi

A simple compile-time dependency injection library for Dart and Flutter

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Kiwi DI for abstract classes

jaydangar opened this issue · comments

Hello, First of all Thank you very much for making job really easy for DI in flutter for the beginners like me, Really appreciate this work. I want to ask that How can I create DI for abstract classes?

I am using Floor package for local database storage in flutter, which requires to create abstract classes for DAO like following. can you let me know how to DI for abstract classes?


import 'package:CookingApp/models/cooks.dart';
import 'package:floor/floor.dart';

@dao
abstract class CookDAO{

  @Insert(onConflict : OnConflictStrategy.replace)
  Future<void> insertCooks(List<Cook> cook);

  @Query('Select * from Cook')
  Future<List<Cook>> fetchAllCooks();
}

First of all, @letsar has created this package, I have taken over the maintenance since 2 weeks. So all the credits go to him.

For your question:

Floor has a floor_generator that generates the non abstract class. If you want to register the abstract class you will have to register an instance that comes from the generated database.

Personally, I have never used floor but by reading the readme on pub.dev I think that is the way to go.

I use moor because it generates the dao classes in a separate file and the dao needs a database. So that will be injected with kiwi as well.

I will have some spare time on Thursday so I will play around with Floor and I will let you know what the best sollution is.

@vanlooverenkoen Thanks, It's okay to use Repository class as a singleton? because repository class contains objects which provides access to API and Database. So, by making Repository class singleton, we will be using a repository and related API and DB as a singleton too. Do I understood this part correct? kindly express your views on it.

Yes indeed. We usually do it like this

Singleton repo's
Singleton services (apis)
Singleton dao's
Singleton database
Factories viewmodels

On top of that every Singleton ia registered as an abstract class. So it is easier to write automated tests

@vanlooverenkoen So, Database class and Dao's in floor are implemented as abstract class only? so does that mean it's singleton by default?

No that does not mean it is a singleton by default. (Im not sure what the correct implementation is for floor. But I would think it is just a normal object. By using kiwi you can make sure the instance is always a singleton. Because kiwi maintains that for you.

@vanlooverenkoen Kindly let me know if there's any way to do it using abstract class, I did't have any example for it. A big thanks in advance.

Hello @vanlooverenkoen, I have give your advice a shot and look into generated floor file, which is as below.

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'cookdb.dart';

// **************************************************************************
// FloorGenerator
// **************************************************************************

class $FloorCookDataBase {
  /// Creates a database builder for a persistent database.
  /// Once a database is built, you should keep a reference to it and re-use it.
  **static _$CookDataBaseBuilder databaseBuilder(String name) =>
      _$CookDataBaseBuilder(name);**

  /// Creates a database builder for an in memory database.
  /// Information stored in an in memory database disappears when the process is killed.
  /// Once a database is built, you should keep a reference to it and re-use it.
  static _$CookDataBaseBuilder inMemoryDatabaseBuilder() =>
      _$CookDataBaseBuilder(null);
}

class _$CookDataBaseBuilder {
  _$CookDataBaseBuilder(this.name);

  final String name;

  final List<Migration> _migrations = [];

  Callback _callback;

  /// Adds migrations to the builder.
  _$CookDataBaseBuilder addMigrations(List<Migration> migrations) {
    _migrations.addAll(migrations);
    return this;
  }

  /// Adds a database [Callback] to the builder.
  _$CookDataBaseBuilder addCallback(Callback callback) {
    _callback = callback;
    return this;
  }

  /// Creates the database and initializes it.
  Future<CookDataBase> build() async {
    final path = name != null
        ? await sqfliteDatabaseFactory.getDatabasePath(name)
        : ':memory:';
    final database = _$CookDataBase();
    database.database = await database.open(
      path,
      _migrations,
      _callback,
    );
    return database;
  }
}

class _$CookDataBase extends CookDataBase {
  _$CookDataBase([StreamController<String> listener]) {
    changeListener = listener ?? StreamController<String>.broadcast();
  }

  CookDAO _cookDAOInstance;

  Future<sqflite.Database> open(String path, List<Migration> migrations,
      [Callback callback]) async {
    final databaseOptions = sqflite.OpenDatabaseOptions(
      version: 1,
      onConfigure: (database) async {
        await database.execute('PRAGMA foreign_keys = ON');
      },
      onOpen: (database) async {
        await callback?.onOpen?.call(database);
      },
      onUpgrade: (database, startVersion, endVersion) async {
        await MigrationAdapter.runMigrations(
            database, startVersion, endVersion, migrations);

        await callback?.onUpgrade?.call(database, startVersion, endVersion);
      },
      onCreate: (database, version) async {
        await database.execute(
            'CREATE TABLE IF NOT EXISTS `Cook` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `cookName` TEXT, `gender` TEXT, `experience` INTEGER, `language` TEXT, `perMonthCharge` INTEGER, `rating` REAL)');

        await callback?.onCreate?.call(database, version);
      },
    );
    return sqfliteDatabaseFactory.openDatabase(path, options: databaseOptions);
  }

  @override
  CookDAO get cookDAO {
    return _cookDAOInstance ??= _$CookDAO(database, changeListener);
  }
}

class _$CookDAO extends CookDAO {
  _$CookDAO(this.database, this.changeListener)
      : _queryAdapter = QueryAdapter(database),
        _cookInsertionAdapter = InsertionAdapter(
            database,
            'Cook',
            (Cook item) => <String, dynamic>{
                  'id': item.id,
                  'cookName': item.cookName,
                  'gender': item.gender,
                  'experience': item.experience,
                  'language': item.language,
                  'perMonthCharge': item.perMonthCharge,
                  'rating': item.rating
                });

  final sqflite.DatabaseExecutor database;

  final StreamController<String> changeListener;

  final QueryAdapter _queryAdapter;

  static final _cookMapper = (Map<String, dynamic> row) => Cook(
      row['id'] as int,
      row['cookName'] as String,
      row['gender'] as String,
      row['experience'] as int,
      row['language'] as String,
      row['perMonthCharge'] as int,
      row['rating'] as double);

  final InsertionAdapter<Cook> _cookInsertionAdapter;

  @override
  Future<List<Cook>> fetchAllCooks() async {
    return _queryAdapter.queryList('Select * from Cook', mapper: _cookMapper);
  }

  @override
  Future<void> insertCooks(List<Cook> cook) async {
    await _cookInsertionAdapter.insertList(cook, OnConflictStrategy.replace);
  }
}

Now in my main function, I have create singleton Object by following.


main(List<String> args) {
  KiwiContainer kiwiContainer = KiwiContainer();
  kiwiContainer.registerSingleton((container) => Repository());
  kiwiContainer.registerSingleton((container) => APIProvider());
  **kiwiContainer.registerSingleton((container) async {
    return await $FloorCookDataBase.databaseBuilder('cook_db.db').build();
  });
  kiwiContainer.registerSingleton((container)=>KiwiContainer().resolve<CookDataBase>().cookDAO);**  
  runApp(MainPage());
}

@vanlooverenkoen can you kindly tell me if my approach is right or not? I am accessing DataBase class through builder and using that singleton Database Object I am accessing DAO object which is defined as a class in CookDataBase abstract class. like following :

@Database(version: 1, entities: [Cook])
abstract class CookDataBase extends FloorDatabase {
  CookDAO get cookDAO;
}

@jaydangar I'm sorry that it took so long. I completely forgot about this ticket. I would do the following

Future<void> main(List<String> args) async{
 await setupDi();
  runApp(MainPage());
}

Future<void> setupDi(){
  KiwiContainer().registerSingleton((container) => Repository());
  KiwiContainer().registerSingleton((container) => APIProvider());
  final db = await $FloorCookDataBase.databaseBuilder('cook_db.db').build()
  KiwiContainer().registerSingleton((container) => db);
  KiwiContainer().registerSingleton((container)=> KiwiContainer().resolve<CookDataBase>().cookDAO);
}

Using the kiwi_generator will result in some extra boilerplate code, that is because of how floor is setup.
Your personDao

import 'package:floor/floor.dart';
import 'package:kiwi_floor/db/database.dart';
import 'package:kiwi_floor/models/person.dart';

@dao
abstract class PersonDao {
  factory PersonDao.fromDb(AppDatabase db) => db.personDao;  //the extra boilerplate code

  @Query('SELECT * FROM Person')
  Future<List<Person>> findAllPersons();

  @Query('SELECT * FROM Person WHERE id = :id')
  Stream<Person> findPersonById(int id);

  @insert
  Future<void> insertPerson(Person person);
}

And your injector file will look like this:

abstract class Injector {
  Future<void> configure() async {
    final db = await $FloorCookDataBase.databaseBuilder('cook_db.db').build()
    KiwiContainer().registerSingleton((container) => db);
    registerDao();
  }

  @Register.singleton(PersonDao, constructorName: 'fromDb') //consturctorName is extra boilerplate code to use with floor
  void registerDao();
}

I personally use Moor for my db integration. It works way better with kiwi. Because the implementation of moor is better for dependency injection.

I hope this answers your question. Sorry for the late response.