Android jetpack Navigationについて簡単な説明

今回は、完全なサンプルとかではなくって、自分のメモ的な感じです。

調べたきっかけは、安易にアクティビティを作ろうとして、Navigation Drawer Activityというのを作ったら、画面遷移について、あまりにわからなかったことです。

で、Android デベロッパーの公式サイト見ると

https://developer.android.com/guide/navigation/

詳細トピックの下にずらーっていろんな項目が並んで、なんか小難しそうな感じじゃないですか!!?
ナビゲーショングラフって言われても、グラフ??棒グラフ?円グラフ?とかでわかりにくいじゃないですか。

しかし、恐れることはありません。

めっちゃ端的に、このナビゲーションで何が便利になったのか、一言でいうと、

画面遷移をxmlで定義することができる

だと思います。

今回も、Googleさんが用意してくれている、Code Labがよいです。英語ですけどね!

https://codelabs.developers.google.com/codelabs/android-navigation/index.html?index=..%2F..%2Findex#0

かいつまんで例を説明します。なお、下記のサンプルコードはこのCodeLabから拝借しています。

①あるアクティビティの中に、Fragmentが入っていて、いくつもFragmentを動かして画面遷移させる。

②Fragmentの親フラグメントみたいなのをまず作ります。

下記がその状態ですが、MainActivityのレイアウトファイルです。
my_nav_host_fragment
というFragmentのレイアウトが、親フラグメントというか、コンテナみたいな感じです。ここに、ほかのFragmentが入れ替わり立ち代わり入るということになります。

<?xml version="1.0" encoding="utf-8"?>

<androidx.drawerlayout.widget.DrawerLayout
    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/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.android.codelabs.navigation.MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/colorPrimary"
            android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />

        <fragment
            android:id="@+id/my_nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:navGraph="@navigation/mobile_navigation" />
    </LinearLayout>

    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:menu="@menu/nav_drawer_menu" />
</androidx.drawerlayout.widget.DrawerLayout>

③ 上記のファイルの、

app:navGraph=”@navigation/mobile_navigation”

がキモです。

mobile_navigation.xml というファイルを作ります。

ここに、画面遷移を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"
    app:startDestination="@+id/home_dest">
    <fragment
        android:id="@+id/home_dest"
        android:name="com.example.android.codelabs.navigation.HomeFragment"
        android:label="@string/home"
        tools:layout="@layout/home_fragment">

        <action
            android:id="@+id/next_action"
            app:destination="@id/flow_step_one_dest"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />
    </fragment>

    <fragment
        android:id="@+id/flow_step_one_dest"
        android:name="com.example.android.codelabs.navigation.FlowStepFragment"
        tools:layout="@layout/flow_step_one_fragment">
        <argument
            android:name="flowStepNumber"
            app:argType="integer"
            android:defaultValue="1"/>

        <action
            android:id="@+id/next_action"
            app:destination="@+id/flow_step_two_dest">
        </action>
    </fragment>

    <fragment
        android:id="@+id/flow_step_two_dest"
        android:name="com.example.android.codelabs.navigation.FlowStepFragment"
        tools:layout="@layout/flow_step_two_fragment">

        <argument
            android:name="flowStepNumber"
            app:argType="integer"
            android:defaultValue="2"/>

        <action
            android:id="@+id/next_action"
            app:popUpTo="@id/home_dest">
        </action>
    </fragment>
<!--中略-->
</navigation>

Fragmentの中に、actionがありますが、actionが画面遷移さきです。アクションというのが画面遷移だと思ってみてください。

たとえばHomeFragmentという画面から、ボタンを押したらFlowStepFragmentに行きたい場合、まずHomeFragment内にアクションを決めておきます。

<fragment
    android:id="@+id/home_dest"
    android:name="com.example.android.codelabs.navigation.HomeFragment"
    android:label="@string/home"
    tools:layout="@layout/home_fragment">

    <action
        android:id="@+id/next_action"
        app:destination="@id/flow_step_one_dest"
        app:enterAnim="@anim/slide_in_right"
        app:exitAnim="@anim/slide_out_left"
        app:popEnterAnim="@anim/slide_in_left"
        app:popExitAnim="@anim/slide_out_right" />
</fragment>

下記の部分が重要で、このflow_step_one_destってのが、

    android:id="@+id/next_action"
    app:destination="@id/flow_step_one_dest"

すぐ下にある

<fragment
    android:id="@+id/flow_step_one_dest"
    android:name="com.example.android.codelabs.navigation.FlowStepFragment"
    tools:layout="@layout/flow_step_one_fragment">
    <argument
        android:name="flowStepNumber"
        app:argType="integer"
        android:defaultValue="1"/>

    <action
        android:id="@+id/next_action"
        app:destination="@+id/flow_step_two_dest">
    </action>
</fragment>

なんですよ。HomeFragment.kt内で下記のようにすれば、

 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val button = view.findViewById<Button>(R.id.navigate_destination_button)
        button?.setOnClickListener {
            findNavController().navigate(R.id.flow_step_one_dest, null)
        }
//後略
}

ボタンを押したらFlowStepFragmentに遷移します。

同じことが、下記のようにしてもできます。

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val button = view.findViewById<Button>(R.id.navigate_destination_button)
        button?.setOnClickListener {
           // findNavController().navigate(R.id.flow_step_one_dest, null)
            val action = HomeFragmentDirections.nextAction()
            findNavController().navigate(action)
        }
//後略
}

AndroidX LiveData,ViewModel, Room,コルーチンを使ってRecycler Viewにリストを表示させる(Kotlin サンプルコードあり)

最初に、私がどういう感じでAndroidの今の開発をやってるかというと、Android2.3~6 になったころぐらいが一番開発をしていて、一旦Androidの開発からは引退してたんですが、最近舞い戻ってきたというところです。

なので、LiveDataとかViewModelとか、実はRecevler Viewも今回初めて実装します。Android Roomとかはもちろん初めてですね。(実はKotlinも初めてです…汗)
というわけで、しばらくやってなかったけど、基本的なことはわかってる。最近のAPIがわからないという人にちょうどいいぐらいの解説を交えて、自分の勉強のためにもまとめを書いておこうと思います。

参考にしたのはこちら様のサイトです。

Android Architecture Components 初級 ( MVVM + LiveData + Coroutines 編 )
https://qiita.com/Tsutou/items/69a28ebbd69b69e51703

Githubにもソースコードがあって、大変助かりました。┌o ペコッ
しかし、これはJSONでデータを受け取って処理するやつですね。
私がやりたかったのは、端末のDBからデータとってきてリストにしたかったことです。なので、Android Roomを使いたかったんです。
しかし、これも後日、とってもいいサンプルを見つけました。

Android Room with a View – Kotlin
https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/#0

これも、Githubにソースコードがあって、大変参考になります。このCodeLabシリーズは非常にクォリティ高いと思います。
それにしても、もう、この記事書く意味ないじゃんワロタ。になりそうでしたが、まぁ英語だし、読むのが骨が折れる人がいると思うので、やっぱり書いておきます。

サンプルなので、複雑なことはなるべく省きます。 また、Roomがなんなのかとか、LiveDataがなんなのかなどの、基本的な説明はしませんがので、そこんとこヨロシク!でお願いします 。

MainActivityだけで全部やります。んで、さんざん他人様のGithubを参考にしてきたので、もちろん私もGithubにこのソースコードを上げておきます。
いろいろ至らないところがあると思いますが、ご指摘ください。

なお、作ったアプリのスクショはこんな感じです。

RoomとLiveDataを使ってタスクをRecyclerViewでリスト表示するアプリ

アプリの目的としては、タスクをDBに追加して、それをRecevler Viewでリストみたいに表示する、ということをやります。

では、前置きがかなり長くなりましたが、まずタスク本体です。Roomでやる場合の決まり事がいっぱいあって、その通りに作ります。

//Task.kt

package jp.onlineconsultant.listsample

import androidx.room.Entity
import androidx.room.PrimaryKey


@Entity(tableName = "task")
data class Task(
@PrimaryKey(autoGenerate = true)
var id: Int? = 0,
var content: String?,
val from_user_id: Int?,
val to_user_id: Int?,
var limit: String?,
var purpose_id: Int?,
var created: String,
var updated: String
) {

override fun toString(): String {

val task_name:String
if(content != null){
task_name = content!!
}else{
task_name = "無題"
}
return task_name
}
}

idが auto generate でprimary keyです。

DBの設定を作ります。

//AppDatabase.kt

package jp.onlineconsultant.listsample.data.database

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import jp.onlineconsultant.listsample.Task
import jp.onlineconsultant.taskadmin.data.dao.TaskDao


@Database(entities = [Task::class],
        version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract val taskDao : TaskDao
}

private lateinit var INSTANCE: AppDatabase
/**
 * Instantiate a database from a context.
 */
fun getDatabase(context: Context): AppDatabase {
    synchronized(AppDatabase::class) {
        if (!::INSTANCE.isInitialized) {
            INSTANCE = Room
                .databaseBuilder(
                        context.applicationContext,
                        AppDatabase::class.java,
                        "task-admin"
                )
                .fallbackToDestructiveMigration()
                .build()
        }
    }
    return INSTANCE
}

@Database(entities = [Task::class],
version = 1) この数字がDBのバージョンの数字ですが、Task.ktなどでプロパティの変更をしたりする場合も、このバージョンを上げないといけません。

//TaskDao.kt

package jp.onlineconsultant.taskadmin.data.dao

import androidx.lifecycle.LiveData
import androidx.room.*
import jp.onlineconsultant.listsample.Task


@Dao
interface TaskDao {

    @Query("SELECT * FROM task ORDER By id DESC LIMIT 30")
    suspend fun taskListNotLiveData(): List<Task>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertOne(task: Task)

    @Delete
    fun delete(task: Task)
    
}

この3つで、なんとデータベースの作成が終わりです。前はSQLiteOpenHelperとか使って、SQL書いてテーブルとか作ってましたが、ずいぶんわかりやすくなりました。

Roomってしゅごい…。

次に、repositoryというファイルを作ります。が、これは必須ではないです。Daoとやり取りするのに、便利な関数をここに入れておく感じなんですかね。


//TaskRepository.kt

package jp.onlineconsultant.listsample.data.repository

import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import jp.onlineconsultant.listsample.R
import jp.onlineconsultant.listsample.Task
import jp.onlineconsultant.taskadmin.data.dao.TaskDao


class TaskRepository(val taskDao: TaskDao) {


suspend fun getTaskList(): List<Task> {

try {

val list = taskDao.taskListNotLiveData()
return list


} catch (cause: Throwable) {

Log.e("TaskRepository", cause.toString())
val list = emptyList<Task>()
return list
}


}

suspend fun addTask(context:Context, task:Task) {

try {

taskDao.insertOne(task)

} catch (cause: Throwable) {

Log.e("TaskRepository", cause.toString())
Toast.makeText(context, "task_cannot_create_db_error" + cause.toString(), Toast.LENGTH_LONG).show();

}
}

}

データベースからデータを取ってくるのはこれぐらいです。

suspend ってついているところが、コルーチンになってます。コルーチン。素晴らしい機能です。ViewModelと連動して、いい感じになりますので、後で説明します。

次に、UIをやりましょう。

いきなりですが、話の都合上、Activityを作ります。

//MainActivity.kt
package jp.onlineconsultant.listsample

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.facebook.stetho.Stetho
import jp.onlineconsultant.listsample.data.database.getDatabase
import jp.onlineconsultant.listsample.data.repository.TaskRepository
import jp.onlineconsultant.listsample.databinding.ActivityMainBinding


class MainActivity : AppCompatActivity() {

    private lateinit var taskListViewModel: TaskListViewModel
    private lateinit var taskListAdapter: TaskListAdapter
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)


        Stetho.initialize(
            Stetho.newInitializerBuilder(applicationContext)
                .enableDumpapp(Stetho.defaultDumperPluginsProvider(applicationContext))
                .enableWebKitInspector(Stetho.defaultInspectorModulesProvider(applicationContext))
                .build()
        )


        setContentView(R.layout.activity_main)

        val database =
            getDatabase(this)
        val repository =
            TaskRepository(database.taskDao)

        taskListViewModel = ViewModelProviders.of(this, TaskListViewModel.Factory(repository))
            .get(TaskListViewModel::class.java)

        taskListAdapter = TaskListAdapter(taskClickCallback)


        //dataBinding用のレイアウトリソース
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        binding.apply {
            taskList.adapter = taskListAdapter
        }

        setRecycleView(binding)

        val button: Button = findViewById(R.id.button)
        button.setOnClickListener {

            taskListViewModel.onButtonClicked(this)

        }

        observeViewModel(taskListViewModel)


    }

    private fun setRecycleView(binding: ActivityMainBinding) {
        val recyclerView: RecyclerView = binding.root.findViewById(R.id.task_list);
        //このレイアウトマネージャーとかの定義がないと、RecyclerViewが表示されない
        recyclerView.setHasFixedSize(true); // RecyclerViewのサイズを維持し続ける
        recyclerView.setLayoutManager(LinearLayoutManager(this));
        recyclerView.setItemAnimator(DefaultItemAnimator());

        // RecyclerView自体の大きさが変わらないことが分かっている時、
        // このオプションを付けておくと、パフォーマンスが改善されるらしい
        recyclerView.setHasFixedSize(true);
    }

    private val taskClickCallback = object : TaskClickCallback {
        override fun onClick(task: Task) {
            //適宜実装してください
        }
    }

    private fun observeViewModel(viewModel: TaskListViewModel) {

        val taskObserver = Observer<List<Task?>?> { tasks ->

            tasks?.let { taskListAdapter.setTaskList(it) }

        }

        viewModel.taskListLiveData.observe(this, taskObserver)


    }

}

次に、レイアウトファイルです。

DataBindingの世界になってから、レイアウトファイルをどうやってうまく作るかは結構問題になってきますね。。。

<!-- activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<layout 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"
>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
>


<androidx.recyclerview.widget.RecyclerView
android:id="@+id/task_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="タスクリスト"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="-16dp"
/>

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="タスクを追加"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/task_list"
/>

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

タスクが一個もないと、タスクをそもそも表示させられないので、タスクを追加できる仕様になってます。追加できるタスクは後で出てきますが、決め打ちのリストからランダムです。

んで、RecyclerViewのアダプタを作ろうとして、
「一体、どうやってリストのデータをBindingするのか…」
そこに詰まりました。

データはDBから非同期で取得されるので、RecyclerViewが作られるときには、データがないんですよ…。

昔だったら、この辺も自作して、オブザーバーとか自分で作ったり、Handlerとか作ったり、AsyncTaskでインターフェース作って画面を更新したりなどなどしていました。

しかし、そんなんならLiveDataとか意味ないじゃん… ってなりまして。検索したところ、冒頭で紹介したQiitaに助けられました。
一旦、作ったLiveDataにDBから値を取得後、値をPostすればいいんです。

んで、そいういうことをやるViewModelを作ります。ViewModelは、ビューにデータを投げます。
例えば、タスクリストを今作ってますが、タスクリストはいろんなActivity、いろんなFragmentで使われる可能性がありますよね。その辺の使いまわしを、今までは自作していたのが、Androidのプラットフォームでこの使いまわしができるようになったのです。


//TaskListViewModel.kt

package jp.onlineconsultant.listsample

import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import jp.onlineconsultant.listsample.data.repository.TaskRepository
import kotlinx.coroutines.launch
import java.util.*
import kotlin.collections.ArrayList

class TaskListViewModel constructor(private val repository: TaskRepository) : ViewModel() {

    val TAG: String = "TaskListViewModel"
    var taskListLiveData = MutableLiveData<List<Task>>()

    init {
        loadTaskList()
    }


    private fun loadTaskList() {
        viewModelScope.launch {
            try {

                Log.d(TAG, "viewModelScope.launch ")
                val listNotLiveData = repository.getTaskList()

                // メンバー変数のLiveDataにこの値を送っている これが大事!!ここは、LiveDataを送るのではなく、リスト形式を送るのがミソ
                taskListLiveData.postValue(listNotLiveData)


            } catch (e: Exception) {

                Log.e(TAG, "データ取得中にエラー " + e)

            }
        }
    }

    fun onButtonClicked(context: Context) {

        //ダミーのタスクリスト
        val task1 = Task(null, "ジョギングする", 1, 1, "2020/02/30", null, "2020/01/30", "2020/01/30");
        val task2 = Task(null, "麻雀する", 1, 1, "2020/02/10", null, "2020/01/30", "2020/01/30");
        val task3 = Task(null, "牛乳を買う", 1, 1, "2020/02/15", null, "2020/01/30", "2020/01/30");
        val task4 = Task(null, "人狼をする", 1, 1, "2020/02/20", null, "2020/01/30", "2020/01/30");

        var task_array = ArrayList<Task>()
        task_array.add(task1)
        task_array.add(task2)
        task_array.add(task3)
        task_array.add(task4)

        viewModelScope.launch {

            val index = Random().nextInt(task_array.size) // ランダムに選択された 0 以上 4 未満の整数
            val result = task_array.get(index)
            addTaskDb(context, result)

            //これはやらないくてもいいのかと思ってましたが、タスクリストをリフレッシュして表示するのに必要です。
            loadTaskList()
        }
    }


    suspend fun addTaskDb(context: Context, task: Task) {
        try {

            repository.addTask(context, task)

        } catch (cause: Throwable) {
            // If anything throws an exception, inform the caller
            Log.e("TaskViewModel", "エラー データベースでデータ追加できない")
            Toast.makeText(context, "データベースエラーで更新できませんでした!", Toast.LENGTH_LONG).show();
        }
    }



    //引数が必要な時は、Factoryが必要
    class Factory(private val repository: TaskRepository) :
        ViewModelProvider.NewInstanceFactory() {
        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return TaskListViewModel(repository) as T
        }
    }

}

ViewModel中の、

viewModelScope.launch {

というのが素晴らしいです。前述で、コルーチンがいい感じになるのを説明しますと書きましたが、ここでかなり真価を発揮します。我々Android開発者の悩み。それは、メインスレッドでDB接続だとかネット接続とかできないので、別スレッドにしたり、AsyncTaskにしたりしてコールバック地獄になり、かなり面倒だったということ。

それが、ViewModelScopeを使ってコルーチンのメソッド呼ぶだけでいいんですよ。ありがたや!

ちな、最初はコルーチンっていいなぁと思ってたんだけど、コルーチンってコルーチンの中からしか呼べないんです。え?ってなりますよね。最初の呼び出し元は、結局Asyncかとか
viewModelScope.launch {} から呼び出さないといけない…みたいですね(参考はこちら)。とりあえず画面にデータ渡す系は、これからは
viewModelScope.launch {}
を使えばよいのです。

RecyclerViewのアダプタを作ります。実のところ、私、アダプタ作るの嫌いなんですよ。Androidで、なんかちょっと複雑なビュー作る時、アダプタ作らないといけないケースが多いですが。仕方ないんですかね(´ω`)。

ここで紹介するのは極めて簡単なアダプタです。一行分のレイアウトファイルをアサインします。

//TaskListAdapter.kt
package jp.onlineconsultant.listsample

import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.RecyclerView
import jp.onlineconsultant.listsample.databinding.TaskListItemBinding

class TaskListAdapter(private val taskClickCallback: TaskClickCallback?) :
    RecyclerView.Adapter<TaskListAdapter.TaskViewHolder>() {

    private var taskList: List<Task?>? = null

    fun setTaskList(taskList: List<Task?>?) {

        this.taskList = taskList

        //これ大事。ないと、データ追加後に画面が更新されません。
        notifyDataSetChanged()

    }

    override fun onCreateViewHolder(parent: ViewGroup, viewtype: Int): TaskViewHolder {
        val binding =
            DataBindingUtil.inflate(
                LayoutInflater.from(parent.context),
                R.layout.task_list_item, parent,
                false
            ) as TaskListItemBinding


        return TaskViewHolder(binding)
    }


    override fun getItemCount(): Int {
        return taskList?.size ?: 0
    }

    open class TaskViewHolder(val binding: TaskListItemBinding) :
        RecyclerView.ViewHolder(binding.root)

    override fun onBindViewHolder(holder: TaskListAdapter.TaskViewHolder, position: Int) {
        holder.binding.task = taskList?.get(position) //task_list_item.xmlの中のtask
        holder.binding.executePendingBindings()
    }
}

次は、タスク1行分のレイアウトファイルです。

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="task"
            type="jp.onlineconsultant.listsample.Task" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <androidx.cardview.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:cardUseCompatPadding="true">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="5dp">

            <TextView
                android:id="@+id/content"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:contentDescription="内容"
                android:text="@{task.content}"
                android:textSize="14dp"
                android:textStyle="bold" />

            <TextView
                android:id="@+id/limit"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:contentDescription="期限"
                android:text="@{task.limit.toString()}"
                android:textSize="14dp" />
        </LinearLayout>
        </androidx.cardview.widget.CardView>
    </LinearLayout>

</layout>

これで、完成です!

いやー。長かった。上記のでできるかと思いますが、Stringファイルとか、AndroidManifestとか、大事な大事なGradleのファイルが必要、という方は、ぜひ下記のGithubに置いてありますので、ご覧ください。

https://github.com/AkikoGoto/list_sample

ご指摘などあれば、バシバシお寄せください。Githubじゃなくって、こちらのブログのほうにご返信いただければありがたいです。m(_ _)m

Android GlideをDataBindingで使うサンプルコード(Kotlin)

Glideは、Androidアプリで画像を表示するときに、簡単にセットしてくれたり、丸く切り抜いてくれる便利なライブラリです。

今回、そのGlideをDatabindingと一緒に使う方法です。

最初、下記を見てやってましたが

https://androidwave.com/loading-images-using-data-binding/

うまくいかなかったので、自分でちょっとアレンジしています。

GlideとDataBinding自体の適用は、いろんなところで情報がありますので、割愛させていただきます。

①まず、Fragmentのレイアウトファイル。DataBinding時代になってから、レイアウトファイルをいかにうまく書くか、は結構難しくなりましたね。

fragment.user_list.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="imageUrl"
            type="String" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">


        <ImageView
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            android:id="@+id/ivProfileImage"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_gravity="center"
            android:layout_margin="5dip"
            android:contentDescription=""
            app:profileImage="@{imageUrl}" />

    </androidx.constraintlayout.widget.ConstraintLayout>


</layout>

UsrListViewModel.kt ビューモデルです。

class UserListViewModel constructor(private val repository: UserRepository) : ViewModel() {

    val TAG: String = "UserListViewModel"
    //監視対象のLiveData
    var userListLiveData = MutableLiveData<List<User>>()



    //引数が必要な時は、Factoryが必要
    class Factory(private val repository: UserRepository) :
        ViewModelProvider.NewInstanceFactory() {
        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return UserListViewModel(repository) as T
        }
    }

  //これが大事 スタティックにしないとエラーが出ます。
    companion object {

        @JvmStatic
        @BindingAdapter("profileImage")
        fun loadImage(view: ImageView, imageUrl: String?) {

       //imageUrlはnullチェックが必要です。
            if(imageUrl != null) {
                Glide.with(view.context)
                    .load(imageUrl).apply(RequestOptions().circleCrop())
                    .into(view)
            }
        }
    }

}

なんで、これスタティックにしないといけないのかなーとか、思ってましたが、スタティックにしないと次のエラーが出ます。

java.lang.IllegalStateException: Required DataBindingComponent is null in class FragmentUserListBindingImpl. A BindingAdapter in jp.onlineconsultant.hogehoge.User is not static and requires an object to use, retrieved from the DataBindingComponent. If you don't use an inflation method taking a DataBindingComponent, use DataBindingUtil.setDefaultComponent or make all BindingAdapter methods static.

imageUrlをnull許可にして、nullチェックしないと、次のエラーも出ます。

Process: jp.onlineconsultant.hogehoge.dev, PID: 19146
java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter imageUrl

③Fragmentです。値をセットします。


UserListFragment.kt

class UserListFragment : Fragment() {

    private lateinit var userListViewModel: UserListViewModel
    private lateinit var binding: FragmentUserListBinding


    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        val database = getDatabase(getActivity() as MainActivity)
        val repository = UserRepository(database.userDao)

        userListViewModel = ViewModelProviders.of(this, UserListViewModel.Factory(repository))
            .get(UserListViewModel::class.java)


        //dataBinding用のレイアウトリソース
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_user_list, container, false)


        setRecycleView(binding)

        return binding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        binding.setImageUrl("https://pbs.twimg.com/profile_images/1162746270697443336/EMUCXT0E_400x400.jpg");
        observeViewModel(userListViewModel)
    }

  

}

Property must be initialized or be abstract

まだまだ全然Kotlin初心者の私です…。

たとえばなんですけど、次のようにメンバー変数を定義したい時に

@Parcelize
class User(val id: Int, val name: String) :Parcelable {

    val reading:String //ここで冒頭のエラー
  val age:Int //ここで冒頭のエラー

}

ってなりますよね。

var reading:String? = null

ってすれば直るんですが。

またはコンストラクタにreading とかageとか入れてもいいです。しかし、呼び出すときに全部入れなきゃいけないの??って思ってました。

これ、

init{}

でやれば、定義の時点でvalでもいけるのに後で気づきました。(;^ω^)

次のような感じです。

@Parcelize
class User(val id: Int, val name: String) :Parcelable {

    val reading:String

    init{
        reading = "hogehoge"
    }
}

Didn’t find class “android.support.v7.widget.RecyclerView” on path: DexPathLis

RecycleViewを使おうとして、上記のエラーに遭遇しました。

実機で実行すると、レイアウトファイルのinflate時にこのエラーでクラッシュします。

数時間ぐらい悩んだんですよねー。

多くのサイトに

import android.support.v7.widget.RecyclerView

import androidx.recyclerview.widget.RecyclerView

にすべしというのが書いてありますがっ。それはやってあるんだよなー。(´ω`)

下記のサイトに答えがありました。

https://tutorialmore.com/questions-296241.htm

レイアウトファイルのRecycleViewの指定を下記から

    <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
             />

次に変更します。

    <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
             />

これで、クラッシュしなくなりました!

P.S こういう自動翻訳サイトって読みづらいので避けていたんですが、最近は、こういうので助かるケースも増えてきました。