12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Dart/Flutter】パッケージ化のすゝめ その1

Last updated at Posted at 2022-01-28

はじめに

株式会社ORPHE(オルフェ)の廣瀬です。
今回はDart/Flutterでパッケージ化をすることのメリットとその方法についてお伝えしたいと思います。

Dartパッケージとは

Dart / Flutterでアプリを作成する際に特に意識せずに使っているかと思いますが、pubspec.yamldependenciesセクションで読み込むライブラリのことをパッケージと呼びます。
Flutterもパッケージ群をインポートしておりStatelessWidgetStatefulWidgetといったFlutterの根幹の機能もすべてFlutter用のパッケージから利用されています。

pub.devなどからインポートするintlProviderといったライブラリもすべてこのパッケージの仕組として作成されており自由にインポート可能です。

パッケージの種類

パッケージには2つの種類があります。

Dart Package

Dartのみで実装された一般的なパッケージ。AndroidやiOSなどのプラットフォームに依存せずに様々な機能を提供します。

Plugin Package

Dartのみではなく、AndroidやiOS向けのネイティブのAPIにアクセスする機能も含めたパッケージ。利用するネイティブの機能によって利用可能なプラットフォームが異なります。

パッケージ化のメリット・デメリット

パッケージ化には下記のメリットデメリットがありますが、基本的にはメリットのほうが大きいので積極的にパッケージ化していったほうがオススメです。

メリット

  • 機能別にコードを整理できるためコードの保守性が高まる
  • すでに作成したコードや機能の使い回しが可能
  • パッケージ化しやすいように設計を考えることができるようになるため分かりやすい設計構造になりやすい

デメリット

  • デバッグなどで不具合が起こった場合、パッケージを横断して修正しなければいけない可能性がある
  • うまく機能を分けられていない場合パッケージ間の整合性が取れず破綻する可能性がある

パッケージ化を行うときの設計方法や考え方に関してはまた別記事で紹介したいと思います。

パッケージ化の方法

具体的にパッケージ化を行うための方法を紹介していきます。

今回はDartコードを機能別に分けて利用する目的としてDart Packageタイプのパッケージとして作成したいと思います。

パッケージプロジェクトを作ろう

下記のコマンドを叩いてみましょう

flutter create --template=package パッケージ名

今回はパッケージ名をtest_packageとします。

flutter create --template=package test_package

Flutterプロジェクトを作成するときのようにパッケージに必要なファイルが作成されます。

基本的にはFlutterのアプリを作成するときと同じ様にpubspec.yamlに他のパッケージの依存関係を記載しlibフォルダ以下にソースコードを作成して開発を進めていきます。

コードを書いてみよう

lib/test_package.dartに下記のサンプルコードが作成されていると思います。

library test_package;

/// A Calculator.
class Calculator {
  /// Returns [value] plus 1.
  int addOne(int value) => value + 1;
}

こちらを書き換えてみましょう。

Flutterプロジェクトを作成するときに下記のカウントアップアプリのサンプルコードが作成されると思いますが、こちらのMyHomePageウィジェットをそのままパッケージの機能として使えるようにしてみます。

import 'package:flutter/material.dart';

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

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

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke "debug painting" (press "p" in the console, choose the
          // "Toggle Debug Paint" action from the Flutter Inspector in Android
          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

lib/test_package.dartを下記の様に書き換えてみましょう。

library test_package;

import 'package:flutter/material.dart';

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

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Column(
          // Column is also a layout widget. It takes a list of children and
          // arranges them vertically. By default, it sizes itself to fit its
          // children horizontally, and tries to be as tall as its parent.
          //
          // Invoke "debug painting" (press "p" in the console, choose the
          // "Toggle Debug Paint" action from the Flutter Inspector in Android
          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
          // to see the wireframe for each widget.
          //
          // Column has various properties to control how it sizes itself and
          // how it positions its children. Here we use mainAxisAlignment to
          // center the children vertically; the main axis here is the vertical
          // axis because Columns are vertical (the cross axis would be
          // horizontal).
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

Calculatorのクラスを削除してしまったことでtest/test_package_test.dartにエラーが出てしまっているかと思うのでこちらを削除します。

これでCounterPageウィジェットを利用可能なパッケージを作成できました。

別プロジェクトでインポートしよう

それでは作成したパッケージをインポートするためのプロジェクトを作成してみましょう。

flutter create test_project

下記のようなフォルダ構成になるようにします。

root
├─test_package
│  │
│  ├─lib
│  │      test_package.dart
│  │
│  │  pubspec.lock
│  │  pubspec.yaml
│  └─ README.md
│
└─test_project
    │
    ├─android
    ├─ios
    ├─web
    ├─lib
    │      main.dart
    │
    │  pubspec.lock
    │  pubspec.yaml
    └─ README.md

test_project/pubspec.yamldependences以下を下記のように書き換えます。

dependencies:
  flutter:
    sdk: flutter

  test_package:
    path: ../test_package

下記のコマンドを実行します。

cd test_project
flutter pub get

これでtest_packageが利用可能になりました。

test_project/lib/main.dartを書き換えてみましょう。

import 'dart:developer';

import 'package:flutter/material.dart';

// パッケージをインポートします。
import 'package:test_package/test_package.dart';

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      // CounterPageを利用します。
      home: const CounterPage(title: 'Flutter Demo Home Page'),
    );
  }
}

これでビルドしてみると問題なくCounterPageを利用しながらアプリの起動ができるはずです!

インポートの種類

pubspec.yamlでのインポート方法には今回のローカルに置かれているパッケージのインポートの他にもいくつか種類があります。

  • pub.devからのインポート
    Providerやintlなどのpub.devに公開されているパッケージをインポートする方法です。

    パッケージ名をそのまま入力します。

    dependencies:
      flutter:
        sdk: flutter
    
      provider: (バージョン※オプション)
    
  • gitからのインポート

    GithubやBitbucketといったgitのホスティングサービスからのインポートが可能です。

    ※基本的にはパブリックレポジトリをインポートすることができます。SSH経由でアクセスしたり認証を通すことによってプライベートレポジトリからもインポート可能ですがここでは割愛します。

    dependencies:
      flutter:
        sdk: flutter
    
      test_package:
    		url: git://github.com/orphe/test_package.git
    

まとめ

いかがでしたでしょうか?
パッケージ化を行うことで実装した機能を分割し使い回しができるようになります。パッケージの作成やインポートも通常のプロジェクトを作成するのと同じ様に簡単に行うことができます。

次回は、有効なパッケージ化のやり方を記事にできたらと思います。

株式会社ORPHEではFlutterエンジニアを募集しています。
興味ある方は是非応募していただけると幸いです。

株式会社ORPHEの会社情報 - Wantedly

12
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?