プログラマになる

組み込みエンジニアで主にC言語、時々C#をやっている人の技術メモです。最近はAndroidも少しずつ勉強中

今更ながらListViewをまとめる - ViewHolder編 -

はじめに

今更ながらListViewをまとめるの続きです。
今回はViewHolderについて説明してみたいと思います。
またPersonクラスを生成してそのプロパティをListViewとViewHolderを使って表示してみます。

1. ListViewに表示するItemを作成

ここは前回と同じくPersonクラスを定義します。

Person.kt

class Person(firstName : String, lastName : String, age : Int) {
    val firstName : String = firstName
    val lastName : String = lastName
    val age : Int = age
}

またPersonクラスを表示するためのLayoutを作成します。
今回も単純にPersonクラスを表示するものしています。

list_item.xml

f:id:kaleidot725:20180624162651p:plain

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:id="@+id/personItem"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView
        android:id="@+id/firstName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="firstName"
        android:background="#FF9999"/>

    <TextView
        android:id="@+id/lastName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="lastName"
        android:background="#99FF99"/>

    <TextView
        android:id="@+id/age"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="age"
        android:background="#9999FF"/>
</LinearLayout>

2.ViewHolderを使ったAdapterを作成する

list_item.xmlをViewとして生成するためにAdapterを作成します。
また今回はViewHolderを実装して、ListViewの表示速度を改善したいと思います。

なぜViewHolderが必要なのか?

まずViewHolderとはなんぞやという感じだと思いますが、
その名の通りViewを保持する役割のクラスです。
じゃあなぜViewHolderでViewを保持する必要があるのでしょうか?
ViewHolderを使わない場合のコードを確認してみましょう。

ListViewAdapter

class ListViewAdapter(fragmentActivity: FragmentActivity?, resource : Int, list: List<Person>) : ArrayAdapter<Person>(fragmentActivity, resource, list) {
    private val inflater : LayoutInflater = LayoutInflater.from(context)

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {

        val view = inflater.inflate(R.layout.list_item, parent, false)

        val firstNameTextView = view.findViewById<TextView>(R.id.firstName)
        firstNameTextView.text = super.getItem(position).firstName

        val lastNameTextView = view.findViewById<TextView>(R.id.lastName)
        lastNameTextView.text = super.getItem(position).lastName

        val ageTextView = view.findViewById<TextView>(R.id.age)
        ageTextView.text = super.getItem(position).age.toString()

        return view
    }
}

getViewが呼ばれるたびにViewを生成してfindViewByIdを使って値をセットしています。
このfindViewByIdがパフォーマンス上問題となります。
findViewByIdは呼び出しコストが高く、回数が多くなるつまりViewが増えるほどパフォーマンスが低下します。
つまりListViewを高速化したい場合はどうにかしてfindViewByIdの呼び出し回数を減らさなければならないということです。
そこでViewHolderが登場することになります。

ViewHolderAdapterを作成する

findViewByIdの呼び出し回数を減らすために新規作成時にfindViewByIdで取得した値をViewHolderに保持しておき、
二回目以降にViewを使う場合にはViewHolderに保存したViewを使うようにします。
これにてfindViewByIdを呼び出す回数が減りListViewを高速化できます。

今回はPersonクラスのプロパティを表示するTextViewを保持するViewHolderを生成します。

class ViewHolder(var firstNameTextView : TextView, var lastNameTextView : TextView, var ageTextView : TextView)

作成したViewHolderを使ってAdapterを実装します。
一回目だけ新規作成、二回目以降はViewHolderの値を利用するように実装します。

class ViewHolderAdapter(fragmentActivity: FragmentActivity?, resource : Int, list: List<Person>) : ArrayAdapter<Person>(fragmentActivity, resource, list) {
    private val inflater : LayoutInflater = LayoutInflater.from(context)

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        lateinit var convertViewTemp : View
        lateinit var viewHolder : ViewHolder

        if (convertView == null) {
            // Viewを作成
            convertViewTemp = inflater.inflate(kaleidot725.listviewlaboratory.R.layout.list_item, parent, false)
          
            // Viewから必要となるTextViewをfindViewByIdで取り出し、ViewHolderに格納しViewのタグにセット
            val firstNameTextView = convertViewTemp.findViewById<TextView>(kaleidot725.listviewlaboratory.R.id.firstName)
            val lastNameTextView = convertViewTemp.findViewById<TextView>(kaleidot725.listviewlaboratory.R.id.lastName)
            val ageTextView = convertViewTemp.findViewById<TextView>(kaleidot725.listviewlaboratory.R.id.age)
            viewHolder = ViewHolder(firstNameTextView, lastNameTextView, ageTextView)
            convertViewTemp.setTag(viewHolder)
        }
        else {
            // タグにセットしておいたViewHolderを取得し、findViewByIdを使わないでTextViewを取得する
            convertViewTemp = convertView          
            viewHolder = convertViewTemp.getTag() as ViewHolder
        }

        viewHolder.firstNameTextView.text = super.getItem(position).firstName
        viewHolder.lastNameTextView.text = super.getItem(position).lastName
        viewHolder.ageTextView.text = super.getItem(position).age.toString()
        return convertViewTemp
    }
}

3.ListViewを作成する

ここは前回と変わりません。
FramgentのLayoutにListViewを追加し表示できるようにします。

fragment_view_holder

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
tools:context=".ViewHolder.ViewHolderFragmenrt">

<ListView
    android:id="@+id/ViewHolderList"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
</ListView>
</FrameLayout>

ViewHolderFragmenrt

class ViewHolderFragmenrt : Fragment() {
    val values : List<Person> = mutableListOf(
            Person("Mike",   "Ryan", 15),
            Person("Fred",   "Griffin", 20),
            Person("Ruke",   "Mohammed", 30),
            Person("ふじい",  "まきこ", 40),
            Person("あずま",  "りさ" , 31),
            Person("りゅう",  "ふみえ" , 32),
            Person("とりうみ","のりこ" , 34),
            Person("はすみ",  "ちづる" , 46),
            Person("もりたに","かおり" , 25),
            Person("すがわら","けいすけ" , 36),
            Person("すみよし","たつや" , 40),
            Person("かつやま","まさのり" , 33),
            Person("させ",   "まさあき" , 27),
            Person("ふない",    "ゆうすけ" , 35),
            Person("じんぐうじ", "ただゆき" , 36),
            Person("かねひら",   "ともはる" , 25),
            Person("おぶち",     "こうじ" , 24),
            Person("あいやま",   "やすゆき" , 18))


    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_view_holder_fragmenrt, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val viewHolderList = view.findViewById<ListView>(R.id.ViewHolderList)
        viewHolderList.adapter = ViewHolderAdapter(activity, android.R.layout.simple_expandable_list_item_1, values)
    }
}

さいごに

このぐらいの表示数ですとそこまでパフォーマンスに影響しないようです。
おそらくですが画像とかをたくさんListViewに配置すると大きな差ができそうです。

f:id:kaleidot725:20180701205642g:plain

github.com

今更ながらListViewをまとめる - 基礎編 -

はじめに

今更ながらListViewの使い方をまとめたいと思います。
今回はPersonクラスを生成してそのプロパティをListViewに表示してみます。

1. ListViewに表示するItemを作成

まずはPersonクラスを定義します。

Person.kt

class Person(firstName : String, lastName : String, age : Int) {
    val firstName : String = firstName
    val lastName : String = lastName
    val age : Int = age
}

Personクラスを表示するためのLayoutを作成します。
今回は単純にPersonクラスを表示するものしてみました。

📝 simple_list_item1など標準で用意されているものもありますが今回は用途と合わないので独自で定義します。

list_item.xml

f:id:kaleidot725:20180624162651p:plain

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:id="@+id/personItem"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView
        android:id="@+id/firstName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="firstName"
        android:background="#FF9999"/>

    <TextView
        android:id="@+id/lastName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="lastName"
        android:background="#99FF99"/>

    <TextView
        android:id="@+id/age"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="age"
        android:background="#9999FF"/>
</LinearLayout>

2.ListViewのAdapterを作成する

list_item.xmlをViewとして生成するためにAdapterを作成します。
以下のようにArrayAdapterを継承したListViewAdapterを作成します。

ListViewAdapter

// コンストラクタで予め、表示したいPersonのリストを指定する。
class ListViewAdapter(fragmentActivity: FragmentActivity?, resource : Int, list: List<Person>) : ArrayAdapter<Person>(fragmentActivity, resource, list) {
    private val inflater : LayoutInflater = LayoutInflater.from(context)

    // ListViewによってViewが必要なときに呼び出される、なのでViewを生成して返してあげる
    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        // Viewを生成して、Adapterのコンストラクタから渡されたPersonListの要素をセットする
        val view = inflater.inflate(R.layout.list_item, parent, false)

        // 生成したViewからTextViewを取得する
        val firstNameTextView = view.findViewById<TextView>(R.id.firstName)
        val lastNameTextView = view.findViewById<TextView>(R.id.lastName)
        val ageTextView = view.findViewById<TextView>(R.id.age)

        // Personリストから値を取得し、セットする
        firstNameTextView.text = super.getItem(position).firstName
        lastNameTextView.text = super.getItem(position).lastName
        ageTextView.text = super.getItem(position).age.toString()

        return view
    }
}

3.ListViewを作成する

そしていよいよ最後です。
FramgentのLayoutにListViewを追加し表示できるようにします。

fragment_list_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    tools:context=".ListView.ListViewFragment">

    <ListView
        android:id="@+id/ListView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </ListView>
</LinearLayout>

次にFragmentのsrcに表示するPersonを生成してListViewにセットします。

ListViewFragment

📝ListView.adapterにListViewAdapterを渡しておく 📝ListViewは渡されたListViewAdapterを使ってViewを生成するのでitem_list.xmlのViewがListに表示される

class ListViewFragment : Fragment() {
    // 表示するリスト生成する
    val values : List<Person> = mutableListOf(
        Person("Mike",   "Ryan", 15),      Person("Fred",   "Griffin", 20), 
        Person("Ruke",   "Mohammed", 30),  Person("ふじい",  "まきこ", 40),
        Person("あずま",  "りさ" , 31),      Person("りゅう",  "ふみえ" , 32),
        Person("とりうみ","のりこ" , 34),     Person("はすみ",  "ちづる" , 46),
        Person("もりたに","かおり" , 25),     Person("すがわら","けいすけ" , 36),
        Person("すみよし","たつや" , 40),     Person("かつやま","まさのり" , 33),
        Person("させ",   "まさあき" , 27),    Person("ふない",    "ゆうすけ" , 35),
        Person("じんぐうじ", "ただゆき" , 36), Person("かねひら",   "ともはる" , 25),
        Person("おぶち",     "こうじ" , 24),  Person("あいやま",   "やすゆき" , 18))

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_list_view, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // PersonリストをセットするListViewを取得する
        val listView = view.findViewById<ListView>(R.id.ListView)
      
        // ListViewAdapterを生成して、そのときにvaluesを引数として渡す
        listView.adapter = ListViewAdapter(activity, R.layout.list_item, values)
    }
}

さいごに

エミュレータで動かすとこんな感じに動きます。
結構スムーズに動作しているように見えますが実機で動かすとカクカクします。

ここでViewHolderを実装する必要性が出てきます。
ViewHolderはListに表示する数が多くなったときのパフォーマンスを改善する手法です。
次回はViewHolderについてまとめたいと思います。

f:id:kaleidot725:20180624163746g:plain

github.com

ViewModelを生成する際にModelを指定する

はじめに

ViewModelProvidersを通してViewModelを生成する場合、 ModelをViewModelに受け渡すには一工夫必要みたいです。 本記事ではModelをViewModelに渡すための実装方法を記載します。

実装

ViewModelProviders.createの呼び出し時にViewModelProvider.Factoryクラスを指定することで ViewModelにModelをDIすることができるようになります。

1.ViewModelを作成する

ViewModelがModelをDIできるようにコンストラクタのパラメータにModelを追加します。

class TextToSpeechDemoViewModel(tts : TextToSpeech) : ViewModel() {
    val tts : TextToSpeech = tts
}

2.ViewModelFactoryを作成する

ViewModelProviders.Factoryを継承し、ModelをViewModelにDIするように実装します。

class ViewModelFactory(tts : TextToSpeech) : ViewModelProvider.Factory{
    val tts : TextToSpeech = tts

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {

        if (modelClass == TextToSpeechDemoViewModel::class.java)
            return TextToSpeechDemoViewModel(tts) as T

        throw IllegalArgumentException("Unknown ViewModel class : ${modelClass.name}")
    }
}

3.ViewModelFactoryをViewModelProvidersにセットしViewModelを生成する

2で作成したViewModelFactoryをViewModelProviers.ofに指定しViewModelを生成します。

class TextToSpeechDemoActivity : AppCompatActivity() {
    var tts : TextToSpeech? = null
    var viewModel : TextToSpeechDemoViewModel? = null
    var viewModelFactory : ViewModelFactory? = null
    
    fun onCreate(savedInstanceState: Bundle?) {
        val tts = android.speech.tts.TextToSpeech(applicationContext, object : TextToSpeech.OnInitListener{
            override fun onInit(p0: Int) {
                if(p0 != android.speech.tts.TextToSpeech.ERROR)
                    tts?.language = Locale.JAPANESE
            }
        })

        viewModelFactory = ViewModelFactory(tts as TextToSpeech)
        viewModel = ViewModelProviders.of(this, viewModelFactory).get(TextToSpeechDemoViewModel::class.java)
    }
}

おわりに

ViewModelProviersからViewModelを生成する場合にModelをDIする方法を載せました。
Modelが多くなった場合にはこの別の実装が必要になるかもしれませんが小規模ならばこれで十分だと思います。

参考記事

https://medium.com/@dpreussler/add-the-new-viewmodel-to-your-mvvm-36bfea86b159

C# XMLファイルの書き込みと読み込み

はじめに

C#XMLファイルの書き込み・読み込みを利用するには下記のクラスを利用する。

名称 説明
XmlWriter XML形式でファイルに書き込む
XmlTextWriter おそらく非推奨でXmlWriterを利用したほうがよい
XmlReader XML形式のファイルを読み込む

サンプルコード

上記のクラスを使ったXMLファイルの書き込みと読み込みのサンプルです。
※書き込むクラスのアクセシビリティはpublicでなければならないので注意が必要です。

public class Person
{
  public string Name {get; set;}
  public string Age { get; set; }
}

public void Save()
{

    var setting = new XmlWriterSettings();
    setting.Indent = true;

    using (var writer = XmlWriter.Create(@"app.xml", setting))
    {
        var serializer = new XmlSerializer(typeof(Person));
        var person = new Person();
        serializer.Serialize(writer, person);
    }
}

public void Load()
{
    using (var reader = XmlReader.Create(@"app.xml"))
    {
        var serializer = new XmlSerializer(typeof(Person));
        var person = serializer.Deserialize(reader);
    }
}

インデント付きで書き込むには?

XmlWriterSettings.Indentをtrueに設定し、
XmlWriterのインスタンスを生成する際に指定します。

    var setting = new XmlWriterSettings();
    setting.Indent = true;
    var writer = XmlWriter.Create(@"app.xml", setting)

コレクションを書き込むには?

内部に配列を持つコレクションクラスを作成します。

  [XmlRoot("Persons")]
  public class PersonCollection
  {
      [XmlElement(Type = typeof(Person), ElementName = "Person")]
      public Person[] Persons { get; set; }
  }
  
  var serializer = new XmlSerializer(typeof(Persons));
  var persons = new Persons();
  serializer.Serialize(writer, persons);

レポジトリパターン: Repository Pattern

レポジトリパターン : Repository Patternとは

データ層へアクセスするコードをレポジトリに記述する。
レポジトリに複雑なデータ取得を
ロジックを集約して管理するパターンらしい。 

データ層へのアクセスをレポジトリに
任せることでビジネスロジックの抽象度が上がり、
ビジネスロジックがより明確になり保守性が向上するとのこと。

またレポジトリを採用することで、
他クラスで単体テストが書きやすくなるなどメリットが多い。
主にデータベース・WEBサービス
アクセスする処理をリポジトリに記述する。

参考記事

プロセス監視ツール(Ressurection)を作った

概要

プロセス監視ツール 「Ressurection」を作りました。
「Ressurection」は登録したソフトウェアのプロセスを監視し、
クラッシュしたら再起動するという機能を持つソフトウェアです。

使い方

使い方は説明するまでもないくらい簡単にしたつもりです。
アプリケーションを起動するとタスクトレイに表示されます。
右クリックで、「Show Setting」で設定画面を開きます。

f:id:kaleidot725:20180403222007p:plain

設定画面で監視するソフトウェアの追加と削除ができます。
+ボタンを押しソフトウェアを追加し
プロセス名称のトグルボタンを押して起動するだけです。
-ボタンを押すとプロセスを削除できます。

f:id:kaleidot725:20180403222012p:plain

登録したプロセスは設定ファイル(setting.json)に保存されます。

[
  {
    "Path": "C:\\Python27\\python.exe"
  }
]

作ってみて

  • 自分が使い慣れているC#WPFで作ったので技術的な苦労はなかった。
  • そもそもGUIアプリケーションを作りたいわけではなく
    テスト駆動開発の練習ネタとして作成した。
  • しかし肝心なテスト駆動開発は主要クラスだけで、
    後はテスト書いてないという状況になってしまった。
  • 今後はViewModelとかも含めてテストコード書ければ良いと思う。
    時間的に余裕ができたり実用性が出てきたらテストを追加したい。

実行ファイルはこちら github.com

最も単純なViewModelとLiveDataの利用

セットアップ

Googleのレポジトリを追加する

buildscriptのrepositoriesにgoogle()を追加する。
以下が設定例です。

buildscript {
    ext.kotlin_version = '1.2.21'
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

Android Architecture Componentを追加する

以下のリストから追加したいAndroid Architecture Componentを選択する

implementation "android.arch.lifecycle:extensions:1.1.1"                  // ViewModel and LiveData
implementation "android.arch.lifecycle:viewmodel:1.1.1"                  // alternatively, just ViewModel
implementation "android.arch.lifecycle:livedata:1.1.1"                       // alternatively, just LiveData
implementation "android.arch.persistence.room:runtime:1.0.0"      // Room
implementation "android.arch.paging:runtime:1.0.0-alpha7"           // Paging
testImplementation "android.arch.core:core-testing:1.1.1"               // Test helpers for LiveData
testImplementation "android.arch.persistence.room:testing:1.0.0" // Test helpers for Room

以下が設定例です。

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "kaleidot725.classicclock"
        minSdkVersion 21
        targetSdkVersion 26
        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"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    implementation "android.arch.lifecycle:extensions:1.1.1"
    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'
}

ViewModelを作成する

今回はLocalDateTimeを1msおきに通知するLiveDataを含んだViewModelを作成する。

class ClockViewModel : ViewModel {
    public val mLocalDateTime: MutableLiveData<LocalDateTime> = MutableLiveData()
    val mTimer : Timer = Timer()

    constructor() {
        mTimer.schedule(object : TimerTask() {
           override  fun run() {
                mLocalDateTime.postValue(LocalDateTime.now())
           }
        }, 1, 1)
    }
}

ActivityにてViewModelを生成する

ViewModelProviders.ofにてViewModelのインスタンスを生成する。
これでViewModelProvidersがViewModelのライフサイクルを
管理してくれるので画面を回転させても問題なくなる。

class MainActivity : AppCompatActivity() {
    var mClockViewModel : ClockViewModel? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mClockViewModel = ViewModelProviders.of(this).get(ClockViewModel::class.java)
    }
}

ViewModelのLiveDataを購読する

ViewModelのLiveDataを購読する。

    mClockViewModel?.mLocalDateTime?.observe(this, object : Observer<LocalDateTime>{
        override fun onChanged(t: LocalDateTime?) {
            val clockTextView = findViewById<TextView>(R.id.ClockText)
            clockTextView.text = t.toString()
        }
    })
}

実行結果

f:id:kaleidot725:20180329235038p:plain

github.com