投稿者「zawata」のアーカイブ

【Flutter】ProviderとConsumerを使った状態管理

概要

Flutterにおいて、状態管理の方法の一つであるProviderConsumerの使い方について紹介する。
サンプルとして、前回の記事【Flutter】GestureDetectorとCustomPaintを使い、タッチした場所に図形を描画するで作成したアプリを使う。まずはこちらを読み、アプリの内容を把握しておいてほしい。

前回はStatefulWidgetとsetState()を使って状態を扱っていたが、それをProviderConsumerを使った方法に書き換えていく。

  • 作成したコードはこちら
    (pointer_drawing_lessonプロジェクトのstate_managementブランチ)

dependencyの追加

ProviderConsumerを使うため、pubspec.yamlのdependencies:にproviderを追加する。

pubspec.yaml

dependencies:
  # ...
  # 省略
  # ...
  provider: ^4.0.0

指定したバージョンは現時点での最新バージョンなので都度調整してほしい。

状態クラスの作成

このアプリでは、状態とはタッチされた点のリストである。そこで、以下のようなクラスを作る。ここでChangeNotifierを継承しているが、このクラスに用意されたnotifyListeners()メソッドを使うと状態の変化を通知することができる。


// 状態クラス タッチされた点を記録する
class PointState extends ChangeNotifier {
  final _points = List();

  // 変更不能なリストのビュー
  UnmodifiableListView get points => UnmodifiableListView(_points);

  void add(Offset offset) {
    _points.add(offset);
    notifyListeners();
  }

  void clear() {
    _points.clear();
    notifyListeners();
  }
}

上記のコードではadd()メソッドの中で、リストに値を追加した後notifyListeners()を呼び、このクラスを使っているWidgetに対して状態の変化を通知している。リストの内容を初期化するclear()メソッドも用意してある。

状態の供給

状態を供給するには、アプリ全体をProviderでラップする必要がある。コードは以下のようになる。


void main() => runApp(ChangeNotifierProvider(
      create: (_) => PointState(),
      child: MaterialApp(
        title: 'Pointer drawing lesson',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: PointerDrawingWidget(title: 'Pointer drawing lesson'),
      ),
    ));

ここでは、runApp()にChangeNotifierProviderを渡している。ChangeNotifierProviderは状態を供給するWidgetの一つで、ChangeNotifierのインスタンスを下位の子Widgetへ供給する。
create:に状態を構築するための関数を渡す。このようにするとchild:以下のWidgetは、Widgetツリーのどの階層にいたとしても後述するProvider.of<T>()を使って状態にアクセスすることができるようになる。

描画Widgetの修正

修正後の描画Widgetは以下のようになる。


class PointerDrawingWidget extends StatelessWidget {
  PointerDrawingWidget({Key key, this.title}) : super(key: key);

  final String title;

  @override
  Widget build(BuildContext context) {
    // 状態の取得
    // 状態が変化したことにより、リビルドが必要なのはCustomPaintのみのため、
    // ここではlisten:falseを指定し、 Scaffold全体がリビルドされるのを回避する
    final pointState = Provider.of(context, listen: false);

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: GestureDetector(
        // TapDownイベントを検知
        onTapDown: (TapDownDetails details) {
          // タッチされた点を追加して状態を更新する
          pointState.add(details.localPosition);
        },
        // 状態のConsumer
        // 状態の変化が通知されると、リビルドされる
        child: Consumer<PointState>(
          builder: (BuildContext context, PointState value, Widget _) {
            // カスタムペイント
            return CustomPaint(
              painter: MyPainter(value.points),
              // タッチを有効にするため、childが必要
              child: Center(),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // タッチした点をクリアする
        onPressed: pointState.clear,
        tooltip: 'Clear',
        child: Icon(Icons.clear),
      ),
    );
  }
}

いくつかのポイントについて説明する。

  • StatelessWidgetを継承するよう修正
    状態はProviderから取得できるため、Widgetで保持する必要が無くなった。そのため、StatelessWidgetへと修正した。
  • 状態の取得
    Widgetのbuild()メソッドの中でProvider.of<T>()メソッドを使い、状態を取得する。この時、オプション引数のlisten:にはfalseを指定している。これは、状態が変化した時(ChangeNotifierのnotifyListeners()が呼ばれた時)にWidgetをリビルドするかしないかを指定するフラグである。デフォルトはtrueであり、この場合はbuild()メソッド内で構築したUI全体がリビルドの対象となる。しかし、このWidget全体(Scaffold以下)の中にはタイトルバーやボタンなど、状態が変化しても表示内容は変わらないWidgetが含まれている。これらのWidgetまでリビルドされてしまうのは無駄である。そのため、それを避けるために明示的にfalseを指定している。
  • 状態の変化によってリビルドが必要なWidgetはConsumerで囲む
    タイトルバーやボタンと違い、CustomPaintは「タッチにより点が追加された」ことをトリガーにして表示を更新する必要がある。そのためにはConsumerをchildとし、そのbuilder:にWidget構築のロジックを書く関数を渡す。関数の第2引数に更新された状態が渡ってくるので、それを使って新しい画面を構築する。

CustomPainter

CustomPainterは前回と変わらない。このクラスは、渡された内容をただ描画することに集中する。


// 描画クラス
class MyPainter extends CustomPainter {
  final List<Offset> _points;
  final _rectPaint = Paint()..color = Colors.blue;

  MyPainter(this._points);

  @override
  void paint(Canvas canvas, Size size) {
    // 記憶している点を描画する
    _points.forEach((offset) => canvas.drawRect(
        Rect.fromCenter(center: offset, width: 20.0, height: 20.0),
        _rectPaint));
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

以上で前回のアプリのリファクタリングが完了した。

参考

Simple app state management(公式ドキュメント・英語)


【Flutter】GestureDetectorとCustomPaintを使い、タッチした場所に図形を描画する

環境

  • Flutter 1.12.16
  • AndroidStudio 3.5

概要

タッチした場所に図形を描画するアプリをFlutterで作成する。完成イメージは以下のようになる。

このアプリの作成を通してGestureDetectorとCustomPaintの基本的な使い方を紹介する。

準備

アプリの作成はAndroidStudioで行った。まずは新規のFlutterプロジェクトを作成し、main.dartを次のように編集してアプリの土台を作る。

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Pointer drawing lesson',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(),
    );
  }
}

描画ウィジェットの作成

それでは描画ウィジェットを作成しよう。このウィジェットが、最終的には先に上記に掲載したコードのMaterialAppのhomeで指定されているScaffoldと置き換わる。描画ウィジェットはタッチした点を覚えておく必要があるため、StatefulWidgetとして作成する。main.dartに以下のコードを追加する。

class PointerDrawingWidget extends StatefulWidget {
  PointerDrawingWidget({Key key, this.title}) : super(key: key);
  
  final String title;

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

このように、状態を持つようなウィジェットを作る場合はStatefulWidgetを継承する。その実装はcreateState()をオーバーライドしてStateクラスを返すことのみを行う。
_PointerDrawingWidgetState()が、このWidgetのためのStateを表すクラスである。dart.mainに、続けて次のコードを実装しよう。

// 描画ウィジェットの状態クラス
class _PointerDrawingWidgetState extends State<PointerDrawingWidget> {
  // タッチした点を覚えておく
  final _points = List<Offset>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: GestureDetector(
        // TapDownイベントを検知
        onTapDown: _addPoint,
        // カスタムペイント
        child: CustomPaint(
          painter: MyPainter(_points),
          // タッチを有効にするため、childが必要
          child: Center(),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // 点のクリアボタン
        onPressed: _clearPoints,
        tooltip: 'Clear',
        child: Icon(Icons.clear),
      ),
    );
  }

  // タッチした点をクリアする
  void _clearPoints(){
    setState((){
      _points.clear();
    });
  }

  // 点を追加
  void _addPoint(TapDownDetails details) {
    // setState()にリストを更新する関数を渡して状態を更新
    setState(() {
      _points.add(details.localPosition);
    });
  }
}

ややコードが長くなったが、主要なポイントを説明する。

  • 4行目でタッチした点を記録しておくためOffsetのListを宣言している。これがこのクラスの状態となる。
  • 12行目、ScaffoldのbodyにはGestureDetectorを指定している。このウィジェットを使うことで、ユーザーのインタラクションを検出できる。ここでは、ユーザーのタッチに反応するため、onTapDownイベントを使った。
  • 12行目、GestureDetectorのchildに指定しているのはCustomPaintである。このウィジェットはCustomPainterを実装することで、背景に自在に描画することができる。実装の詳細は後述する。
  • 19行目ではCustomPainterのchildにCenterウィジェットを置いている。このようにしないと、GestureDetectorがタッチイベントに反応しないため、あえてこのようにしている。領域全体を占めるウィジェットであれば、Centerでなくても良いと思う。
  • 39行目の_addPaint()はonTapDownイベント発生時に呼ばれるメソッドで、タッチされた座標をリストに追加している。Flutterでの決まりごととして、状態を更新するにはStateに用意されているsetState()メソッドに、更新のための関数を渡すことで行う必要がある。

次に、カスタム描画のコードを示す。


class MyPainter extends CustomPainter{
  final List<Offset> _points;
  final _rectPaint = Paint()..color = Colors.blue;

  MyPainter(this._points);

  @override
  void paint(Canvas canvas, Size size) {
    // 記憶している点を描画する
    _points.forEach((offset) =>
        canvas.drawRect(Rect.fromCenter(center: offset, width: 20.0, height: 20.0), _rectPaint));
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

カスタム描画を行うときはCustomPainterを継承したクラスを作り、paint()とshouldRepaint()をオーバーライドする。paint()には実際の描画コードを書く。引数で渡ってくるCanvasには色々な描画メソッドが用意されている。ここでは、タッチした点を中心として大きさ20の四角形を描画している。この辺りの使い方はAndroidネイティブの描画方法とほぼ同じなため、すぐに馴染めると思う。shouldRepaint()はboolを返すメソッドで、再描画すべき時にはtrueを返す必要がある。このように簡単なサンプルでは常にtrueを返しても良いだろう。

これで、実装は完了した。最後に冒頭のMaterialAppのhomeを描画ウィジェットに置き換えるとアプリの完成である。


class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Pointer drawing lesson',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      // MaterialAppのhomeに作成した描画ウィジェットを指定する
      home: PointerDrawingWidget(title:'Pointer drawing lesson'),
    );
  }
}

このアプリの完全なコードはgithubにあるので参考にして欲しい。


【Android】Navigation componentを使う

概要

Android Jetpackに含まれるNavigation componentを使うと、画面間の遷移を実装する上で複雑になりがちな点を軽減できる。今回は、このコンポーネントを使って簡単な画面遷移を実装してみる。

サンプルアプリ

環境

Android Studio 3.5.1

準備

build.gradle(app)に以下の依存を追加する。

dependencies {
    ...
    implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0'
    implementation 'androidx.navigation:navigation-ui-ktx:2.1.0'
    ....
}

ライブラリーのバージョンはブログ執筆時点での最新バージョンのため、必要に応じて適宜更新してほしい。

ナビゲーションの追加

プロジェクトのresフォルダの下に新たにnavigationフォルダを作成し、ナビゲーションリソースxmlを追加する。ここでは、公式サンプルに倣ってnav_graph.xmlという名前にした。このファイルを選択すると下記のような画面が立ち上がる。ここでは、Fragmentを2つ作り、1つ目のFragmentから2つ目のFragmentへ遷移を示す矢印を引いている。

xmlは以下のようになっている。

nav_graph.xml

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph.xml"
    app:startDestination="@id/myFragment">
    <fragment
        android:id="@+id/myFragment"
        android:name="com.zawapro.navigationcomponentlesson.MyFragment"
        android:label="fragment_my"
        tools:layout="@layout/fragment_my" >
        <action
            android:id="@+id/action_myFragment_to_myFragment22"
            app:destination="@id/myFragment2" />
    </fragment>
    <fragment
        android:id="@+id/myFragment2"
        android:name="com.zawapro.navigationcomponentlesson.MyFragment2"
        android:label="fragment_my_fragment2"
        tools:layout="@layout/fragment_my_fragment2" />
</navigation>

上記の過程で、ナビゲーションエディタから2つのFragmentが作られる。これらのxmlを下記のように編集する。

1つ目は画面右下にボタンがあるFragment。

fragment_my.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimary"
    tools:context=".MyFragment">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginRight="16dp"
        android:layout_marginBottom="16dp"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

もう一つは遷移先の画面で、特に何もない画面である。

fragment_my_frgment2.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/design_default_color_primary"
    tools:context=".MyFragment2"/>

次に、Activityが読み込むレイアウトファイルactivity_main.xmlを下記のように変更する。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_host_fragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    app:defaultNavHost="true"
    app:navGraph="@navigation/nav_graph">
</fragment>

fragmentタグを使い、全面にFragmentを配置している。ポイントは6行目でNavHostFragmentを指定しているところである。このようにしておくと、NavHostFragmentは11行目app:navGraphで指定したidのナビゲーションリソースを読み込み、それに従った画面とナビゲーションを構築してくれる。10行目ではapp:defaultNavHost="true"としている。このようにすることで、デフォルトの画面から遷移した後、バックキーを押すとその画面に戻ってくることができる。

ここまで出来たら画面の遷移のコードを書こう。先のnav_host.xmlにて初期画面をMyFragmentに設定していた。そこで、MyFragmentのコードを以下のように編集する。

MyFragment.kt

class MyFragment : Fragment() {
  override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View? {
    // Inflate the layout for this fragment
    return inflater.inflate(R.layout.fragment_my, container, false).also { layout ->
      layout.findViewById<Button>(R.id.button).setOnClickListener {
        // NavControllerを呼び出し、アクションを指定して遷移する
        findNavController().navigate(R.id.action_myFragment_to_myFragment22)
      }
    }
  }
}

inflater.inflate(R.layout.fragment_my, container, false)also()で処理を追加している。その中では、findNavController()でNavControllerを取得し、そのnavigate()メソッドを使いアクション先を指定する。こうすると、指定した先の画面へと遷移が行われる。

以上がNavigation componentの使い方のサンプルである。
ここまでのソースをgithubにアップしたので、参考にしてほしい。


【Android】 OssLicensesMenuActivityに表示されるライセンス情報を手動で追加する

概要

Google Play ServicesのIncluding Open Source Noticesにあるツールを使うと、プロジェクトで使用しているオープンソースライブラリのライセンスリストを自動生成し、OssLicensesMenuActivityから表示させることができる。

 しかし、このツールはgradleに記載したライブラリしかリストアップしてくれない。 例えば、material iconなどのような、gradleの依存関係に記載できないプロダクトのライセンスはリストアップの対象外である。そこで、何とかこのツールを使いつつ、ライセンス項目を追加する方法を考えてみた。

今回作成したコードの完成版はこちら

環境

Android Studio 3.2.1

手順

まずは、空のAndroidプロジェクトを作り、OssLicensesMenuActivityを表示するところまでをやってみる。ここまでの手順は以下のサイトを参考にした。

まずルートレベルのbuild.gradleにプラグインのクラスパスを追加する。

build.gradle (root-level)

buildscript {
  repositories {
    // ...
    google()
  }
  dependencies {
    // ...
    // 追加
    classpath 'com.google.android.gms:oss-licenses-plugin:0.9.3'
  }
}

アプリケーションレベルのbuild.gradleの冒頭にプラグインを適用させる旨を追記する。

build.gradle (app-level)

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'
// 追加
apply plugin: 'com.google.android.gms.oss-licenses-plugin'

同じくアプリケーションレベルのbuild.gradleのdependenciesブロックの中に、oss-licensesライブラリーを追加する。

implementation 'com.google.android.gms:play-services-oss-licenses:16.0.2'

デフォルトのActivityに適当なButtonを追加し、ボタンをクリックするとライセンス画面を表示するようにしてみる。

MainActivity.kt

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // OssLicensesMenuActivityにインテントを投げる
        findViewById<Button>(R.id.button).setOnClickListener{
            startActivity(Intent(this, OssLicensesMenuActivity::class.java))
        }
    }
}

ここまでの手順で、gradleのdependenciesに記述されたライブラリのライセンスを表示させることができた。

ライセンス情報の追加

さて、本題はここからである。gradleの依存関係に無いプロダクトのライセンス情報を手動で追加するにはどうすればよいだろうか?GitHubに公開されているプラグインのソースを眺めながらビルド時に何が行われるかを確認していこう。処理の大まかな流れとしては、

  • preBuild前に下記のタスクが実行されるように登録
    (OssLicensesPlugin.groovy)
  • gradleの依存情報を走査し、
    app/build/generated/third_party_licenses/res/dependencies.jsonに書き込む(DependencyTask.groovy)
  • 上記のjsonファイルの内容を元に
    app/build/generated/third_party_licenses/res/raw以下にthird_party_license_metadatathird_party_licensesを生成(LicensesTask.groovy)

ここまでの流れで、app/build/generated/third_party_licenses/resの構成は以下のように自動生成される。

├── dependencies.json
└── res
    └── raw
        ├── third_party_license_metadata
        └── third_party_licenses

third_party_licensesには依存情報から取得された全てのライセンスの具体的な内容が、そしてthird_party_license_metadataには以下の形式でコンテンツのメタ情報が書かれている。

コンテンツの開始位置:コンテンツの文字数 ライセンス名

その後、OssLicensesMenuActivityは起動時に上記の2つのファイルの内容を読み込み、リストビューに表示させているのである。

さて、ということはビルドプロセス中に、これらのファイルに任意の文字列を書き込んでしまえばライセンス情報を追加できそうである。そこで、アプリレベルのbuild.gradleに以下のコードを追加した。

build.gradle (app-level)

// ライセンス情報を追加するタスク
task addMyLicenseTask {
    final String UTF_8 = "UTF-8"
    final byte[] LINE_SEPARATOR = System.getProperty("line.separator").getBytes(UTF_8)

    // generateLicensesタスクの直後に起動
    mustRunAfter tasks.findByName('generateLicenses')

    // 手動でライセンス情報を追加する
    doLast {
        def dependencyOutput = new File(project.buildDir, "generated/third_party_licenses")

        def resourceOutput = new File(dependencyOutput, "/res")
        def outputDir = new File(resourceOutput, "/raw")

        // ライセンスファイル
        def licensesFile = new File(outputDir, "third_party_licenses")
        // ライセンスファイルへの書き込み前に現在の位置を保持
        def start = licensesFile.length()

        // ライセンスファイルへ書き込み
        def licenseContent = 'This is my library license!!'
        licensesFile << licenseContent
        licensesFile << (LINE_SEPARATOR)

        // ライセンスメタデータファイルに書き込み
        def licensesMetadataFile = new File(outputDir, "third_party_license_metadata")
        // "開始位置:文字数 ライセンス名"の形式で書き込む
        licensesMetadataFile << ("${start}:${licenseContent.length()} my_library")
        licensesMetadataFile << (LINE_SEPARATOR)
    }
}

// preBuild前にライセンス情報を追加する
tasks.findByPath(':app:preBuild').dependsOn addMyLicenseTask

とりあえず今回はmy_libraryという適当な文字列を追加している。
これをビルドして実行すると、意図通りライセンス情報が追加された。

※表示の順番はコンテンツファイルに書かれていた順番ではなく、ソートされている。

あとは、必要に応じて表示する内容を調整していけばいいだろう。
ここまでの完全なコードはGitHubにプッシュしたので参考にしてほしい。


【Android】Android Architecture Componentsを使う – LifeCycle編

最近、Android Architecture Componentsの1.0安定板が公開された。
まだ情報は少なく、APIの変更も頻繁に行われているようである。しかし、このライブラリはAndroidアプリを開発していく上での新たなコーディングスタイルの指針となるものであり、ネイティブアプリの開発者にとっては今後必修項目となるように感じられる。そこで、まずは Componentsのうちのひとつ、Lifecycleについて見ていくことにしたい。

Lifecycleとは

ある任意のクラスをActivityのライフサイクルに連動させることができる。これを使うと、これまではActivityやFragmentに書きがちだったActivity本来の処理とは関係のないアプリ特有のロジックを専用のクラスに移すことが容易になる。ActivityやFragmentの肥大化を防ぎ、各ロジックを受け持つクラスの責務がはっきりすることでコードの保守性が高まる。

準備

公式サイトにbuild.gradleにライブラリの依存を追加する方法が書いてある。しかし、実は最新のサポートライブラリ(本記事では27.1.1)を導入済みであれば、特に追加の設定なく使うことができた。この記事で作成したアプリのbuild.gradleを示す。

build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "sample.com.lifecyclelesson"
        minSdkVersion 14
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}

これは、Android Studio 3.1で新しいプロジェクトを作成した時に自動的に作られたbuild.gradleそのものである。

ライフサイクルに連動させるクラスの作成

以下のように作成した。

MyClass.java
public class MyClass implements LifecycleObserver{
    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    void handleCreate(){
        Log.d("LifecycleLesson", "ON_CREATE");
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    void handleDestroy(){
        Log.d("LifecycleLesson", "ON_DESTROY");
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    void handlePause(){
        Log.d("LifecycleLesson", "ON_PAUSE");
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    void handleResume(){
        Log.d("LifecycleLesson", "ON_RESUME");
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    void handleStart(){
        Log.d("LifecycleLesson", "ON_START");
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    void handleStop(){
        Log.d("LifecycleLesson", "ON_STOP");
    }
}

ライフサイクルに連動させるには、LifecycleObserverをimplementsする必要がある。とは言うものの、LifecycleObserverは空のインターフェースに過ぎないので実装すべきメソッドは無い。意味合いとしては「ライフサイクルを観測するクラスである」ということを明記する、といったところだろう。あとは、ライフサイクルイベントが発生した時に呼び出されてほしい任意のメソッドに@OnLifecycleEventアノテーションを付与するだけである。メソッドは引数無しvoidの形式を守っていれば、名前は何でも良い。アノテーションの引数にはライフサイクルイベントの種類を指定する。

LifeCycleObserverの登録

Activityに先ほどのクラスを登録する。コードは以下のようになる。

MainActivity.java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.d("LifecycleLesson", "Activity::onCreate");

        // Lifecycleを取得し、LifecycleObserverをimplementsしたクラスを渡す
        getLifecycle().addObserver(new MyClass());
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        Log.d("LifecycleLesson", "Activity::onDestroy");
    }

    @Override
    protected void onPause() {
        super.onPause();

        Log.d("LifecycleLesson", "Activity::onPause");
    }

    @Override
    protected void onResume() {
        super.onResume();

        Log.d("LifecycleLesson", "Activity::onResume");
    }

    @Override
    protected void onStart() {
        super.onStart();

        Log.d("LifecycleLesson", "Activity::onStart");
    }

    @Override
    protected void onStop() {
        super.onStop();

        Log.d("LifecycleLesson", "Activity::onStop");
    }
}

最新のサポートライブラリのAppCompatActivityにはLifecycleを取得するためのgetLifecycle()メソッドがある。ActivityのonCreateのタイミングでLifecycleを取得し、addObserver()で登録することができる。

実行

アプリを起動、終了させると以下のようにLogcatに出力された。


04-11 23:01:58.680 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: Activity::onCreate
04-11 23:01:58.685 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: ON_CREATE
04-11 23:01:58.686 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: Activity::onStart
    ON_START
04-11 23:01:58.687 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: Activity::onResume
04-11 23:01:58.688 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: ON_RESUME
04-11 23:02:32.518 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: ON_PAUSE
    Activity::onPause
04-11 23:02:34.134 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: Activity::onResume
    ON_RESUME
04-11 23:03:33.507 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: ON_PAUSE
    Activity::onPause
04-11 23:03:33.979 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: ON_STOP
    Activity::onStop
04-11 23:03:33.980 20664-20664/sample.com.lifecyclelesson D/LifecycleLesson: ON_DESTROY
    Activity::onDestroy

Activityのイベントに連動して独自クラスのメソッドが呼び出されたことがわかる。

まとめ

Lifecycleを使うことで、簡単にActivityのライフサイクルに連動するクラスを作成することができた。今回はログを出力しただけだったが、実際にはアプリ特有の処理をこちらのクラスに書いていくことになるだろう。これまでのサンプルやチュートリアルで見られたActivityのon〜()系メソッドをオーバーライドして専用の処理を書いていたスタイルは今後、一変すると思う。最新のサポートライブラリに含まれているこの機能を積極的に使っていかない手はない。


【Xamarin.Forms】SkiaSharpを使って描画の実装を共通化する

概要

Xamarin.Formsを使うとマルチプラットフォーム向けアプリのUI実装が共通化できるのだが、デフォルトではボタンやテキストボックス等、必要最低限なコントロールしか用意されていない。
ちょっと凝ったことをしたい場合、例えば描画処理を自分で実装しようと思ったら各プラットフォームごとに書くしかないのである。
しかし、せっかくXamarinを使うのだったら、なるべくコードは共通化したい。
そこで、SkiaSharpというライブラリを使ってPCL側で描画処理を実装するようにしてみる。

環境

macOS Sierra
Visual Studio Community 2017 for Mac

準備

Visual Studioを起動し、新しいソリューションを作成する。
ここでは、ソリューション名を「HelloSkia」とした。プロジェクトのテンプレートは
Multiplatform -> アプリ -> Xamarin.Forms -> Blank Forms App
を選択する。

ソリューションが作成されると、中にPCL, Android向け, iOS向けのプロジェクトができているので、
すべてのプロジェクトに必要なパッケージを追加する。今回は、
プロジェクトを右クリック -> 追加 -> NuGetパッケージの追加SkiaSharp for Xamarin.Formsを選ぶ。
この時点で、PCLプロジェクトにはSkiaSharp, SkiaSharp.Views.Forms, AndroidとiOSプロジェクトにはSkiaSharp, SkiaSharp.Views, SkiaSharp.View.Formsのパッケージが追加されていればよい。

実装

xamlの実装
HelloSkiaという名前でソリューションを作成した場合、PCLプロジェクトにHelloSkiaPage.xamlというファイルが生成されているはずである。まずは、このファイルを編集する。

HelloSkiaPage.xaml
<ContentPage 
    xmlns="http://xamarin.com/schemas/2014/forms" 
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
    xmlns:local="clr-namespace:HelloSkia"
    xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
    x:Class="HelloSkia.HelloSkiaPage">
    <StackLayout VerticalOptions="FillAndExpand">
        <Label Text="Welcome to Xamarin Forms!" VerticalOptions="Center" HorizontalOptions="Center" />
        <skia:SKCanvasView x:Name="canvasView" PaintSurface="Handle_PaintSurface" HeightRequest="400" />
    </StackLayout>
    
</ContentPage>

注目するのはハイライトした部分である。
xmlns:skia=”clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms”
として、以降skia:という名前でSkiaSharpのクラスにアクセスできるようにする。
そして、StackLayoutに1つのSKCanvasViewを追加している。SKCanvasViewはカスタムの描画処理を実装できるViewで、Xamarin.Formsの他のViewと同じようにレイアウトに含めたりできる。
このタグの中にあるPaintSurfaceというのはSKCanvasViewの描画イベントである。
ここにHandle_PaintSurfaceというメソッド名を指定している。このようにすると描画が要求されたタイミングでこのメソッドが呼ばれるようになる。

コードビハインドの実装
次にコードビハインドを実装していこう。xamlがHelloSkiaPage.xamlという名前であれば、コードビハインドのファイルはHelloSkiaPage.xaml.csという名前でHelloSkiaPage.xamlの一階層下にあるはずである。実装内容は以下のようにした。

HelloSkiaPage.xaml.cs
using SkiaSharp;
using Xamarin.Forms;

namespace HelloSkia
{
    public partial class HelloSkiaPage : ContentPage
    {
        private SKPaint rectPaint = new SKPaint
        {
            StrokeWidth = 4,
            IsAntialias = true,
            Color = SKColors.Blue
        };
        private SKPaint circlePaint = new SKPaint
        {
            StrokeWidth = 4,
            IsAntialias = true,
            Color = SKColors.Green
        };
        private SKPaint linePaint = new SKPaint
        {
            StrokeWidth = 4,
            IsAntialias = true,
            Color = SKColors.Red
        };


        public HelloSkiaPage()
        {
            InitializeComponent();
        }

        // 描画メソッド。
        void Handle_PaintSurface(object sender, SkiaSharp.Views.Forms.SKPaintSurfaceEventArgs e)
        {
            var canvas = e.Surface.Canvas;

            // 四角形を描画
            canvas.DrawRect(new SKRect(10, 10, 200, 200), rectPaint);
            // 円を描画
            canvas.DrawCircle(320, 110, 100, circlePaint);
            // 線を描画
            canvas.DrawLine(430, 10, 630, 210, linePaint);
            canvas.DrawLine(630, 10, 430, 210, linePaint);
        }
    }
}

描画処理は先ほどのxamlで指定しておいたHandle_PaintSurfaceメソッドに書いて行く。
ここでは、四角形、円、ラインといった簡単な図形を描画してみた。APIの使い方は、AndroidでonDrawを実装するときとほぼ一緒という感覚である。
これを各プラットフォームのシミュレータで実行すると以下のようになる。

Android
iOS

ここまでで、AndroidとiOSのプロジェクト内には何らソースコードを書いていない。このようにすることで、AndroidとiOSで描画処理を共通化することができた。

参考サイト・記事

Xamarin.FormsとSkiaSharpを組み合わせたチュートリアル。英語だが、SkiaSharpを使う上で最小限のことがまとまっていてわかりやすい。
https://blog.xamarin.com/drawing-with-skiasharp/

自サイト内記事。AndroidでのViewの描画方法について。
https://zawapro.com/?p=331


【iOS】UIViewをタッチして移動させる

使用環境
Xcode:8.0
Swift:3.0

UIViewをタッチして移動させてみよう。完成イメージは以下の通りである。
20161022_viewtouchmove
※クリックでGIFアニメーションを表示

UIViewControllerが用意したデフォルトのUIViewにサブビューを追加し、赤いビューと青いビューをタッチして動かせるようになっている。

準備

Xcodeを起動したら「Single View Application」テンプレートで新しいプロジェクトを作成する。

実装

プロジェクト内にあるViewController.swiftに処理を書いていく。
以下にコードを示す。

ViewController.swift

import UIKit

// ユーティリティメソッド CGPoint同士の足し算を+で書けるようにする
func -(_ left:CGPoint, _ right:CGPoint)->CGPoint{
    return CGPoint(x:left.x - right.x, y:left.y - right.y)
}
// ユーティリティメソッド CGPoint同士の引き算を-で書けるようにする
func +(_ left:CGPoint, _ right:CGPoint)->CGPoint{
    return CGPoint(x:left.x + right.x, y:left.y + right.y)
}

class ViewController: UIViewController {

    // タッチ開始時のUIViewのorigin
    var orgOrigin: CGPoint!
    // タッチ開始時の親ビュー上のタッチ位置
    var orgParentPoint : CGPoint!
    
    // Viewのパンジェスチャーに反応し、処理するためのメソッド
    func handlePanGesture(sender: UIPanGestureRecognizer){
        switch sender.state {
        case UIGestureRecognizerState.began:
            // タッチ開始:タッチされたビューのoriginと親ビュー上のタッチ位置を記録しておく
            orgOrigin = sender.view?.frame.origin
            orgParentPoint = sender.translation(in: self.view)
            break
        case UIGestureRecognizerState.changed:
            // 現在の親ビュー上でのタッチ位置を求める
            let newParentPoint = sender.translation(in: self.view)
            // パンジャスチャの継続:タッチ開始時のビューのoriginにタッチ開始からの移動量を加算する
            sender.view?.frame.origin = orgOrigin + newParentPoint - orgParentPoint
            break
        default:
            break
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ビューを2つ作成し、Subviewとして追加する
        // !!注意!!
        // UIGestureRecognizerのインスタンスは複数のViewで使いまわせないので、各View毎に作成する
        let view1 = UIView(frame: CGRect(x:10,y:20,width:100,height:50))
        view1.backgroundColor = UIColor.red
        view1.addGestureRecognizer(
            UIPanGestureRecognizer(target:self, action:#selector(handlePanGesture)))
        self.view.addSubview(view1)
        
        let view2 = UIView(frame: CGRect(x:10,y:100,width:100,height:50))
        view2.backgroundColor = UIColor.blue
        view2.addGestureRecognizer(
            UIPanGestureRecognizer(target:self, action:#selector(handlePanGesture)))
        self.view.addSubview(view2)
        
    }
}

アプリ起動時にまず呼ばれるのは、38行目からのviewDidLoadである。
ここでは背景色が赤と青のビューを一つずつ作成し、親にサブビューとして追加している。

これらのビューでタッチイベントを受けられるようにするため、UIGestureRecognizerの一種であるUIPanGestureRecognizerをサブビューに追加する。これはパンジェスチャ、すなわちタッチ後にドラッグするような操作を認識するためのRecognizerである。
ここで注意が必要なのはGestureRecognizerのインスタンスは複数のUIViewで使い回せない、ということである。下のようについ書きたくなるのだが、このように書くとタッチが有効になるのは最後に追加したViewの方のみとなってしまう。

(GestureRecognizer追加時の失敗例)

let recognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture))
let view1 = UIView()
view1.addGestureRecognizer(recognizer)

// こちらのViewだけがタッチを受け取る
let view2 = UIView()
view2.addGestureRecognizer(recognizer)

タッチ時の処理を扱うのは20行目からのhandlePanGestureメソッドである。ここで行っているのは以下の処理である。

  • タッチ開始時:タッチされたビューの位置(origin)と親ビュー上のタッチ位置を記録しておく。
  • タッチ移動時:タッチ開始時点のビューの位置(origin)に、タッチ開始点から現在のタッチまでの移動量を足してビューの新しい位置(origin)とする。

タッチの移動量は
「現在の親ビューにおけるタッチ位置」-「タッチ開始時点の親ビューにおける位置」
で求めることができる。サブビューの位置はタッチの移動によってどんどん変わっていくから、計算には親ビュー上のタッチ位置の値を使うことがポイントである。

31行目でCGPointに対して+や-の演算子を使っている。本来はCGPointにこのような演算子はないのだが、4行目、6行目でユーティリティメソッドを用意しているのでこのように書くことができる。これは、ソースコードを見やすくするための工夫である。もし、普通に書くならば以下のようになるだろう。

sender.view?.frame.origin = 
    CGPoint(x:(orgOrigin.x + newParentPoint.x - orgParentPoint.x,
            y:(orgOrigin.y + newParentPoint.y - orgParentPoint.y ))

CGPointのユーティリティメソッドについて、さらに詳しく知りたいときは以下の記事が参考になる。
http://qiita.com/koher/items/7b4f23d749d973d409c9

さて、今回の内容は以前にAndroidでもやったことがある。Androidでの実装に興味がある方は以下の記事も参考にしてほしい。
https://zawapro.com/?p=305


【iOS】PlaygroundでUIViewを表示する

使用環境
Xcode:8.0
Swift:3.0

XcodeにはPlaygoundという機能が搭載されている。
iOSアプリ開発の場合、エミュレータや実機を立ち上げることなく書いたコードの実行結果がすぐに表示されるので、便利である。そこで、今回はUIViewをPlaygroundで表示させてみよう。

準備

Xcodeを起動したら File -> New -> Playground を選択し、新規Playgroundを作成する。

実装

作成されたPlaygroundに以下のように入力する。
とりあえず、UIViewを表示させたいので以下のようにした。

MyPlayground.playground

import UIKit
import PlaygroundSupport

// UIViewを生成する。大きさはPlaygroundなので適当
let view = UIView(frame: CGRect(x:0, y:0, width:360, height:480))
// 背景に薄灰色を表示
view.backgroundColor = UIColor.lightGray
// ライブビューにセット
PlaygroundPage.current.liveView = view

2行目 PlaygroundSupportというフレームワークをインポートしている。これは、9行目に出てくるPlaygroundPageを使うためのもので、かつてはXCPlaygroundという名前だった。
4-7行目 UIViewを生成する。大きさは適当な値を与えている。プロトタイピングの段階ではこれでいいだろう。また、実行結果を分かりやすくするため、背景色には薄い灰色を指定した。
9行目 UIViewを表示させるためのコードである。直前で作ったUIViewをliveViewプロパティにセットする。

ここまで入力できたら、アシスタントビューを表示させてみよう。アシスタントビューは右上の丸が二つ重なっているアイコンをクリックすると表示できる。
assistantview

うまくいくと、アシスタントビューにUIViewが表示される。
playgdound


【Android】Fragmentを使う(1) レイアウトに直接配置する

Fragmentを効果的に使うことで、画面構成が柔軟になり、UI部分のコードの再利用がしやすくなる。
しかし、使い方を理解するには少し複雑である。
そこで、Fragmentの使い方について、小さなところから順に見ていくことにする。
今回紹介するのは「Fragmentをレイアウトにタグで直接配置する方法」である。

まずはAndroidStudioで新しいプロジェクトを作る。
プロジェクトのテンプレートは、「Empty Activity」とする。
プロジェクトが作成されたら、以下の手順で進めていく。

1.FragmentのためのUIレイアウトを作る
まずは、FragmentのためのUIレイアウトを作る。
my_fragment.xmlという名前でres/layout下に保存し、内容は適当に以下のようにした。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ea6c6c">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="This is my fragment!"
        android:id="@+id/textView"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="New Button"
        android:id="@+id/button"
        android:layout_below="@+id/textView"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />
</RelativeLayout>

テキストとボタンを配置しただけの単純なレイアウトである。
配置された場所を分かりやすくするため、全体の背景色を赤系にしてある。

2.Fragmentを継承したクラスを作る
次は、Fragmentのためのソースを作成する。
ソースコードは以下のようになる。

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class MyFragment extends Fragment {
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        // 引数で渡ってきたinflaterを使ってレイアウトをinfrateする
        final View layout = inflater.inflate(R.layout.my_fragment, container);

        // 必要に応じてUIにコールバックを設定する
        layout.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getContext(), "Hello Fragment!!", Toast.LENGTH_SHORT).show();
            }
        });

        // infrateされたViewをそのまま返す
        return layout;
    }
}

注意が必要なのは、android.app.Fragmentではなく、android.support.v4.app.Fragmentをインポートしているところである。(3行目)
これは、サポートライブラリに含まれるFragmentである。
あえてこちらのパッケージを使う理由は、Androidのバージョンが低い端末向けにアプリを配信する場合でも、最新の機能が反映されたFragmentを使えるからである。
apkのサイズが大きくなってしまうのとトレードオフだが、特に理由が無ければサポートライブラリ版を使ったほうが良いと思う。

さて、実装については今回は必要最低限とするため、onCreateView()のオーバーライドのみとした。
引数で渡ってくるinfratorを使ってレイアウトをinfrateしてやり、必要に応じてUIにコールバックをセットした後、infrateされたViewを返すだけである。
以上がFragmentの作成である。

3.メインのレイアウトにFragmentを配置する
Activityが読み込むためのレイアウトファイルに、fragmentタグを使って直接配置する。
ソースは以下のようになる。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.zawapro.fragmentlesson.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:id="@+id/textView2" />

    <fragment
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:name="com.zawapro.fragmentlesson.MyFragment"
        android:id="@+id/fragment"
        android:layout_below="@+id/textView2"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true" />
</RelativeLayout>

ハイライトした18行目がFragmentを配置している個所である。
このように、普通のViewを配置する方法と何ら変わりは無いことが分かる。

4.Activityのコード
Activityのコードは以下のようになる。今回はAndroidStudioが自動生成したものに手を加えていない。
サポートライブラリを使うため、AppCompatActivity となっている点に気を付けておけば良いだろう。

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

以上でアプリは完成である。
実行すると以下のようになる。
device-2016-04-21-231706

ところで、配置したFragmentはどのようにViewに置き換わっているのだろうか?
Viewのヒエラルキーを見ると以下のようになっている。
hierarchy
activity_main.xmlで宣言したfragmentの部分が、そのままmy_fragment.xmlの内容に置き換わっているのが分かる。
まずは「FragmentとはViewのまとまりをコンポーネント化する仕組みである」ということをおさえておくと良いと思う。

さて、とは言うものの、実はレイアウトファイルにタグで直接Fragmentを配置する方法は固定的であまり融通が利かないのである。
例えば、画面上のFragmentを動的に入れ替えたり、バックスタックに乗せたり、ということをするにはそのためのコードを書いていく必要がある。
そういった意味では今回紹介した方法ではあまりFragmentにする恩恵を受けているとは言えず、
実際ほとんど使われていないのではないか、という気がする。
しかし、入門編としてはこのくらいの内容が良いだろう。


【Android】ndk-buildの実行時に情報を出力する

NDKのソースをndk-buildコマンドでビルドをするとき、Android.mkに書いた内容が間違っていてビルドが上手くいかないことがある。
こうしたときはエラーメッセージを調べつつ原因を探していくことになるのだが、ビルドがどこまで進んだか、とかビルド中に使われた変数の値がどうなっているか、などが分かれば間違った原因を探るための手掛かりになる。
そうした用途に便利なのが$(error), $(warning), $(info)である。これらは元々Makefileの関数だが、Android.mkでもそのまま使える。実際には以下のようにして使う。

$(error エラー時のメッセージ)
$(warning ワーニング時のメッセージ)
$(info メッセージ)

早速試してみよう。
まずは適当なフォルダを作り、以下のような構成にする。

{project_folder}
└── jni
    └── Android.mk

jniフォルダはNDKのデフォルトのソースフォルダで、本来はこの下にc/c++で書いたソースコードを置いていく。
このフォルダが無いと、ndk-buildを実行したときに「 Could not find application project directory !」というエラーとなってしまうので、とりあえず今は空のフォルダを作っておく。

Android.mkには以下の内容を記述する。

HOGE := this is hoge!

$(info This is info mesage! HOGE:$(HOGE))
$(warning This is warning mesage!)
$(error This is error mesage!)

上記では$(info)の例として、変数HOGEにセットされた値を表示している。
ファイルを保存したら、プロジェクトのルートフォルダでndk-buildコマンドを実行する。
結果は以下のようになる。

$ pwd
{project_folder}
$ ndk-build
This is info mesage! HOGE:this is hoge!
jni/Android.mk:4: This is warning mesage!
jni/Android.mk:5: *** This is error mesage!.  中止.

この実行結果から以下のことが分かる。
・infoは単にメッセージを出力する。
・warningはメッセージに加え、ファイル名と行数を表示する。
・errorはメッセージに加え、ファイル名と行数を表示するとともに、その個所で停止する。

これらの関数を効果的に使うことで、不具合が起きている個所を特定するのが容易になるだろう。