Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kiwi DI for abstract classes #46

Closed
jaydangar opened this issue Jun 23, 2020 · 8 comments
Closed

Kiwi DI for abstract classes #46

jaydangar opened this issue Jun 23, 2020 · 8 comments
Assignees
Labels
kiwi kiwi package question from customer Question from the customer on how to use the packages

Comments

@jaydangar
Copy link

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();
}

@vanlooverenkoen
Copy link
Collaborator

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 vanlooverenkoen self-assigned this Jun 23, 2020
@vanlooverenkoen vanlooverenkoen added question from customer Question from the customer on how to use the packages kiwi kiwi package labels Jun 23, 2020
@jaydangar
Copy link
Author

@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.

@vanlooverenkoen
Copy link
Collaborator

vanlooverenkoen commented Jun 23, 2020

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

@jaydangar
Copy link
Author

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

@vanlooverenkoen
Copy link
Collaborator

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.

@jaydangar
Copy link
Author

@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.

@jaydangar
Copy link
Author

jaydangar commented Jun 23, 2020

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;
}

@vanlooverenkoen
Copy link
Collaborator

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kiwi kiwi package question from customer Question from the customer on how to use the packages
Projects
None yet
Development

No branches or pull requests

2 participants