0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【チュートリアル動画の紹介】MVVM Habit Tracker App Tutorial in Android Studio

Posted at

MVVM - Kotlin Android Studio

MVVMアーキテクチャ

  • 「Model」(モデル)
  • 「View」(ビュー)
  • 「ViewModel」(ビューモデル)

Observerによる表示の更新

  • ViewModel
  • LiveData
  • Observer

Room、ViewModel、LiveData を使用したアプリの設計・構築方法

image.png
引用:アーキテクチャ コンポーネントの概要図

チュートリアル - MVVM Habit Tracker App Tutorial in Android Studio

画面遷移 - androidx.navigation
データベース - androidx.room

1. 生活習慣アプリの紹介

MVVM Habit Tracker App Tutorial in Android Studio (Introduction)

In this app series we will be using MVVM along with ROOM, ViewModels, LiveData, and other Navigational components that make life easier.

アプリの概要と使い方とを説明しています。

2. UIまわりの構成と準備

MVVM Habit Tracker App Tutorial in Android Studio (Setting up the UI)

Welcome to the second part of the MVVM Habit Tracker Tutorial! In this video we will be going over how to creat the UI for our app. And remember, you can find all the necessary code in my Github repository : https://github.com/indently/Habit_Tracker_2020

  1. app/build.gradle ファイルの編集
  2. build.gradle ファイルの編集
  3. Fragment(blank) の追加(リスト表示、追加、更新)と設定(画面遷移)
  4. アイコンの追加と設定(res/drawable
  5. メニューバーの追加と設定(res/menu
  6. レイアウトの追加と設定(res/layout

GitHub リポジトリ

GitHub: https://github.com/indently/Habit_Tracker_2020

app/build.gradleファイルの編集

→ Setting up the UI (14秒)

プラグインの追加

build.gradle (Modlue: app)

apply plugin: "kotlin-kapt"
apply plugin: "androidx.navigation.safeargs.kotlin"

コンパイルオプションの設定

build.gradle (Modlue: app)

    // Compile options
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }

依存モジュールの追加

  1. Coroutines
  2. Navigation Components
  3. Lifecycle Components
  4. Retrofit2
  5. Room Components
  6. ViewPager2
  7. Circle Indicator (ViewPager2)
build.gradle (Modlue: app)

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

    //Coroutines
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'

    // Navigation Components
    def nav_version = "2.3.5"
    implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
    implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

    // Lifecycle Components
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
    implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
    implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"

    //Retrofit2
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

    // Room Components
    implementation "androidx.room:room-runtime:2.3.0"
    kapt "androidx.room:room-compiler:2.3.0"
    implementation "androidx.room:room-ktx:2.3.0"
    androidTestImplementation "androidx.room:room-testing:2.3.0"

    //ViewPager2
    implementation 'androidx.viewpager2:viewpager2:1.0.0'

    //Circle Indicator (ViewPager2)
    implementation 'me.relex:circleindicator:2.1.4'

}

build.gradleファイルの編集

→ Setting up the UI (2分37秒)

classpathの追加

build.gradle (Project)

        def nav_version = "2.3.0"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"

Gradle Sync(再同期)

→ Setting up the UI (3分4秒)

Sync Project with Gradle Filesアイコンで、プラグインを再同期します。

カラーの定義

→ Setting up the UI (3分20秒)

app/src/main/res/values/colors.xmlファイルのカラー定義を変更します。

colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#66BB6A</color>
    <color name="colorPrimaryDark">#26A69A</color>
    <color name="colorAccent">#66BB6A</color>
</resources>

resの構築

navigation - app/src/main/res/navigation

→ Setting up the UI (3分40秒)

main_nav.xmlファイルで、fragmentを追加し、それぞれのリンクを定義します。

  1. habitList
  2. createHabitItem
  3. updateHabitItem

drawable - app/src/main/res/drawable

→ Setting up the UI (5分58秒)

vectorselectorを定義します。

  • ハンバーガーアイコン(セレクターあり)
  • ティカップアイコン(セレクターあり)
  • 禁煙アイコン(セレクターあり)
  • 削除アイコン
  • 追加アイコン
  • ラウンドボタン(shape

menu - app/src/main/res/menu

→ Setting up the UI (10分2秒)

  • 全削除アイテム(nav_main.xml
  • 削除アイテム(single_item_menu.xml

layout - app/src/main/res/layout

→ Setting up the UI (11分45秒)

  • activity_main.xmlNavHostFragmentをアタッチメント
  • recycler_habit_item.xmlの追加(CardView
  • intro_item_page.xmlの追加
  • fragment_habit_list.xmlの編集(RecyclerView, FloatingActionButton
  • fragment_create_habit_item.xmlの編集(ScrollView
  • fragment_update_habit_item.xmlの編集(idに"_update"を付与)

3. Roomデータベースとビューモデル

MVVM Habit Tracker App Tutorial in Android Studio (Room Database & ViewModel)

What is up guys? This is the 3rd part of the MVVM Habit Tracker tutorial. In this video we will be organising some packages and creating our database and viewmodel so that we can access our data later on.

  1. Packageの追加
  2. fragmentsパッケージの編集
  3. データクラスの追加
  4. データアクセスオブジェクトの追加

Packageの追加(フォルダ構造)

→ Room Database & ViewModel (10秒)

  1. uilogicdataパッケージ(フォルダ)を追加
  2. ui内にfragmentsパッケージを追加
  3. ui/fragments内にcreatehabithabitlistupdatehabitパッケージを追加
    (ktファイルを各パッケージに移動)
  4. ui内にviewmodelsintroscreenパッケージを追加
  5. logic内にdaorepositoryutilsパッケージを追加
  6. data内にdatabasemodelsパッケージを追加
app/src/main/java/com/federicocotogno/habittracker2020
.
├─ [habittracker2020]
│   ├─ [data]
│   │   ├─ [database]
│   │   └─ [models]
│   ├─ [logic]
│   │   ├─ [dao]
│   │   ├─ [repository]
│   │   └─ [utils]
│   └─ [ui]
│       ├─ [fragments]
│       │   ├─ [createhabit]
│       │   │   └─  CreateHabitItem.kt
│       │   ├─ [habitlist]
│       │   │   └─  HabitList.kt
│       │   └─ [updatehabit]
│       │   │   └─  UpdateHabitItem.kt
│       ├─ [introscreen]
│       ├─ [viewmodels]
│       └─  MainActivity.kt

.

fragmentsパッケージの編集

→ Room Database & ViewModel (2分50秒)

  • ui/fragments/createhabit/CreateHabitItem.kt
  • ui/fragments/habitlist/HabitList.kt
  • ui/fragments/updatehabit/UpdateHabitItem.kt

データクラスの追加(data.models

Habitデータクラス(エンティティ)

→ Room Database & ViewModel (3分38秒)

data/models/Habit.kt
@Parcelize
@Entity(tableName = "habit_table")
data class Habit(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val habit_title: String,
    val habit_description: String,
    val habit_startTime: String,
    val imageId: Int)
: Parcelable

IntroViewデータクラス

→ Room Database & ViewModel (5分12秒)

data/models/IntroView.kt
data class IntroView(val description: String, val image: Int) {
}

データアクセスオブジェクトの追加(logic.dao)

HabitDaoインターフェース(DAO)

→ Room Database & ViewModel (5分38秒)

logic/dao/HabitDao.kt
@Dao
interface HabitDao {

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun addHabit(habit: Habit)

    @Update
    suspend fun updateHabit(habit: Habit)

    @Delete
    suspend fun deleteHabit(habit: Habit)

    @Query("SELECT * FROM habit_table ORDER BY id DESC")
    fun getAllHabits(): LiveData<List<Habit>>

    @Query("DELETE FROM habit_table")
    suspend fun deleteAll()
} 

Roomデータベース抽象クラスの追加(data.database

→ Room Database & ViewModel (8分5秒)

data/database/HabitDatabase.kt

@Database(entities = [Habit::class], version = 1, exportSchema = false)
abstract class HabitDatabase : RoomDatabase() {

    abstract fun habitDao(): HabitDao

    companion object {
        @Volatile
        private var INSTANCE: HabitDatabase? = null

        fun getDatabase(context: Context): HabitDatabase {
            val tempInstance = INSTANCE
            if (tempInstance != null) {
                return tempInstance
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    HabitDatabase::class.java,
                    "habit_database"
                ).build()
                INSTANCE = instance
                return instance
            }
        }
    }
}

DAOを返す抽象メソッド
抽象メソッド(abstract fun habitDao(): HabitDao)を定義していますが、Roomデータベースは、@Daoごとにこの抽象メソッドを介して DAO を公開します。

リポジトリクラスの追加(logic.repository

→ Room Database & ViewModel (10分15秒)

logic/repository/HabitRepository.kt
class HabitRepository (private val habitDao: HabitDao) {
    val getAllHabits: LiveData<List<Habit>> = habitDao.getAllHabits()

    suspend fun addHabit(habit: Habit) {
        habitDao.addHabit(habit)
    }

    suspend fun updateHabit(habit: Habit) {
        habitDao.updateHabit(habit)
    }

    suspend fun deleteHabit(habit: Habit) {
        habitDao.deleteHabit(habit)
    }

    suspend fun deleteAllHabits() {
        habitDao.deleteAll()
    }

}

リポジトリの必要性
リポジトリは、複数のデータソースを管理するために使用されます。このチュートリアルでは、データソースが 1 つのみであるため、その効果を十分発揮していません。
https://developer.android.com/codelabs/android-room-with-a-view-kotlin?hl=ja#8

ビューモデルクラスの追加(ui.viewmodels

→ Room Database & ViewModel (11分43秒)

ui/viewmodels/HabitViewModel.kt
class HabitViewModel(application: Application) : AndroidViewModel(application) {

    private val repository: HabitRepository
    val getAllHabits: LiveData<List<Habit>>


    init {
        val habitDao= HabitDatabase.getDatabase(application).habitDao()
        repository = HabitRepository(habitDao)

        getAllHabits = repository.getAllHabits
    }

    fun addHabit(habit: Habit) {
        viewModelScope.launch(Dispatchers.IO) {
            repository.addHabit(habit)
        }
    }

    fun updateHabit(habit: Habit) {
        viewModelScope.launch(Dispatchers.IO) {
            repository.updateHabit(habit)
        }
    }

    fun deleteHabit(habit: Habit) {
        viewModelScope.launch(Dispatchers.IO) {
            repository.deleteHabit(habit)
        }
    }

    fun deleteAllHabits() {
        viewModelScope.launch(Dispatchers.IO) {
            repository.deleteAllHabits()
        }
    }

}

4. 日時計算とナビゲーション

MVVM Habit Tracker App Tutorial in Android Studio (Calculating time between two dates)

In this video we'll be creating an object that will take care of all the calculations we need to make this app calculate the time between two dates and return a formatted string.

日時計算

→ Calculating time between two dates

開始日時・経過日時等の日時処理を行う関数を定義しています。

  • calculateTimeBetweenDates()
  • cleanDate()
  • cleanTime()
logic/utils/Calculations.kt
package com.federicocotogno.habittracker2020.logic.utils

import java.sql.Date
import java.sql.Timestamp
import java.text.SimpleDateFormat

object Calculations {

    //todo: Change it so it returns a string to display to the textView of the habit item
    fun calculateTimeBetweenDates(startDate: String): String {

        val endDate = timeStampToString(System.currentTimeMillis())

        val sdf = SimpleDateFormat("dd/MM/yyyy HH:mm")
        val date1 = sdf.parse(startDate)
        val date2 = sdf.parse(endDate)

        var isNegative = false

        var difference = date2.time - date1.time
        if (difference < 0) {
            difference = -(difference)
            isNegative = true
        }

        val minutes = difference / 60 / 1000
        val hours = difference / 60 / 1000 / 60
        val days = (difference / 60 / 1000 / 60) / 24
        val months = (difference / 60 / 1000 / 60) / 24 / (365 / 12)
        val years = difference / 60 / 1000 / 60 / 24 / 365

        if (isNegative) {

            return when {
                minutes < 240 -> "Starts in $minutes minutes"
                hours < 48 -> "Starts in $hours hours"
                days < 61 -> "Starts in $days days"
                months < 24 -> "Starts in $months months"
                else -> "Starts in $years years"
            }
        }

        return when {
            minutes < 240 -> "$minutes minutes ago"
            hours < 48 -> "$hours hours ago"
            days < 61 -> "$days days ago"
            months < 24 -> "$months months ago"
            else -> "$years years ago"
        }
    }

    private fun timeStampToString(timeStamp: Long): String {

        val stamp = Timestamp(timeStamp)
        val sdf = SimpleDateFormat("dd/MM/yyyy HH:mm")
        val date = sdf.format(Date(stamp.time))

        return date.toString()
    }

    fun cleanDate(_day: Int, _month: Int, _year: Int): String {
        var day = _day.toString()
        var month = _month.toString()

        if (_day < 10) {
            day = "0$_day"
        }

        if (_month < 9) { //Because the month instance we retrieve starts at 0 and it's stupid!
            month = "0${_month + 1}"
        } else if (_month >= 9 && _month <= 11) {
            month = (_month + 1).toString()
        }

        return "$day/$month/$_year"
    }

    fun cleanTime(_hour: Int, _minute: Int): String {
        var hour = _hour.toString()
        var minute = _minute.toString()

        if (_hour < 10) {
            hour = "0$_hour"
        }
        if (_minute < 10) {
            minute = "0$_minute"
        }
        return "$hour:$minute"
    }
}

ナビゲーション(NavHostFragment

→ Calculating time between two dates (10分28秒)

既にres/layout/activity_main.xmlで、NavHostFragmentをアタッチメントしていますので、それをMainActivityクラスで、設定します。

ui/MainActivity.kt
class MainActivity : AppCompatActivity() {

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

        setupActionBarWithNavController(findNavController(R.id.navHostFragment))
    }

    override fun onSupportNavigateUp(): Boolean {
        val navController = findNavController(R.id.navHostFragment)
        return navController.navigateUp() || super.onSupportNavigateUp()
    }
    
    :
    
}

Codelab: Jetpack Navigation
NavHostFragmentウィジェットやNavigation コンポーネントについては、次のCodelabで学習できます。

Jetpack Navigation
https://developer.android.com/codelabs/android-navigation

5. 生活習慣項目の追加

MVVM Habit Tracker App Tutorial in Android Studio (Creating a Habit)

Finally we can start creating our habits! In this video we will create the habits and in the next video we will show them in our recyclerview.

画面遷移 - リスト画面から追加画面へ

→ Creating a Habit (18秒)

リスト画面の追加ボタン(fab_add)で、リスト画面から追加画面へ遷移します(R.id.action_habitList_to_createHabitItem)。

ui/fragments/habitlist/HabitList.kt

class HabitList : Fragment(R.layout.fragment_habit_list) {
    :
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)   
        :
        
        fab_add.setOnClickListener {
            findNavController().navigate(R.id.action_habitList_to_createHabitItem)
        }
        
        :
    }
    :
}

追加画面の実装

  1. 日付選択リスナーと時刻選択リスナー
  2. 内部変数(項目値)の初期化
  3. ViewModelの取得(HabitViewModel)
  4. 選択アイコンの実装
  5. 日付選択と時刻選択の実装
  6. 登録ボタンの実装

日付選択リスナーと時刻選択リスナー

→ Creating a Habit (55秒)

TimePickerDialog.OnTimeSetListenerインターフェースとDatePickerDialog.OnDateSetListenerインターフェースをCreateHabitItemクラスで実装します。

内部変数(項目値)の初期化

→ Creating a Habit (1分36秒)

項目の入力値を一時保持する内部変数を初期化します。

永続化項目

  • title
  • description
  • drawableSelected
  • timeStamp

現在日付

  • day
  • month
  • year

現在時刻

  • hour
  • minute

選択日付・選択時刻

  • cleanDate
  • cleanTime

ViewModelの取得(HabitViewModel)

→ Creating a Habit (2分31秒)

HabitViewModelインスタンスを取得し、内部保持します。データベースへの登録に使用します(habitViewModel.addHabit())。

ui/fragments/createhabit/CreateHabitItem.kt

class CreateHabitItem : Fragment(R.layout.fragment_create_habit_item),
    TimePickerDialog.OnTimeSetListener, DatePickerDialog.OnDateSetListener {
    :

    private lateinit var habitViewModel: HabitViewModel

    @RequiresApi(Build.VERSION_CODES.N)
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

        habitViewModel = ViewModelProvider(this).get(HabitViewModel::class.java)

        :
    }
    :
}

選択アイコンの実装

→ Creating a Habit (3分58秒)

選択アイコンの選択状態を切り替える処理を実装し、選択されたアイコンを内部保持します(drawableSelected)。

ui/fragments/createhabit/CreateHabitItem.kt
    :
    private var drawableSelected = 0
    :

    @RequiresApi(Build.VERSION_CODES.N)
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        :
        
        //Selected and image to put into our list
        drawableSelected()
    }

    // Create a selector for our icons which will appear in the recycler view
    private fun drawableSelected() {
        iv_fastFoodSelected.setOnClickListener {
            iv_fastFoodSelected.isSelected = !iv_fastFoodSelected.isSelected
            drawableSelected = R.drawable.ic_fastfood

            //de-select the other options when we pick an image
            iv_smokingSelected.isSelected = false
            iv_teaSelected.isSelected = false
        }

        iv_smokingSelected.setOnClickListener {
            iv_smokingSelected.isSelected = !iv_smokingSelected.isSelected
            drawableSelected = R.drawable.ic_smoking2

            //de-select the other options when we pick an image
            iv_fastFoodSelected.isSelected = false
            iv_teaSelected.isSelected = false
        }

        iv_teaSelected.setOnClickListener {
            iv_teaSelected.isSelected = !iv_teaSelected.isSelected
            drawableSelected = R.drawable.ic_tea

            //de-select the other options when we pick an image
            iv_fastFoodSelected.isSelected = false
            iv_smokingSelected.isSelected = false
        }

    }

    :

日付選択と時刻選択の実装

→ Creating a Habit (6分54秒)

  • 現在日時の取得 - getTimeCalendar() , getDateCalendar()
  • リスナーの実装 - onTimeSet() , onDateSet()
  • 日時選択ボタン - pickDateAndTime()
ui/fragments/createhabit/CreateHabitItem.kt
    :

    @RequiresApi(Build.VERSION_CODES.N)
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        :
        
        //Pick a date and time
        pickDateAndTime()

        :
    }

    :
    
    @RequiresApi(Build.VERSION_CODES.N)
    //set on click listeners for our data and time pickers
    private fun pickDateAndTime() {
        btn_pickDate.setOnClickListener {
            getDateCalendar()
            DatePickerDialog(requireContext(), this, year, month, day).show()
        }

        btn_pickTime.setOnClickListener {
            getTimeCalendar()
            TimePickerDialog(context, this, hour, minute, true).show()
        }
    }

    :

    //get the time set
    override fun onTimeSet(TimePicker: TimePicker?, p1: Int, p2: Int) {
        Log.d("Fragment", "Time: $p1:$p2")

        cleanTime = Calculations.cleanTime(p1, p2)
        tv_timeSelected.text = "Time: $cleanTime"
    }

    //get the date set
    override fun onDateSet(p0: DatePicker?, yearX: Int, monthX: Int, dayX: Int) {

        cleanDate = Calculations.cleanDate(dayX, monthX, yearX)
        tv_dateSelected.text = "Date: $cleanDate"
    }

    //get the current time
    private fun getTimeCalendar() {
        val cal = Calendar.getInstance()
        hour = cal.get(Calendar.HOUR_OF_DAY)
        minute = cal.get(Calendar.MINUTE)
    }

    //get the current date
    private fun getDateCalendar() {
        val cal = Calendar.getInstance()
        day = cal.get(Calendar.DAY_OF_MONTH)
        month = cal.get(Calendar.MONTH)
        year = cal.get(Calendar.YEAR)
    }

    :

登録ボタンの実装(HabitViewModelでデータベースへ追加)

→ Creating a Habit (12分58秒)

  • 登録ボタンのイベントハンドラ - btn_confirm.setOnClickListener {...}
  • 入力値を内部変数に代入
  • 入力検証
  • HabitViewModelインスタンスでデータベースへ追加
  • リスト画面へ遷移 - R.id.action_createHabitItem_to_habitList
ui/fragments/createhabit/CreateHabitItem.kt
    :

    private var title = ""
    private var description = ""
    private var drawableSelected = 0
    private var timeStamp = ""

    private lateinit var habitViewModel: HabitViewModel

    :

    private var cleanDate = ""
    private var cleanTime = ""


    @RequiresApi(Build.VERSION_CODES.N)
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

        habitViewModel = ViewModelProvider(this).get(HabitViewModel::class.java)

        //Add habit to database
        btn_confirm.setOnClickListener {
            addHabitToDB()
        }
        
        :
    }

    private fun addHabitToDB() {

        //Get text from editTexts
        title = et_habitTitle.text.toString()
        description = et_habitDescription.text.toString()

        //Create a timestamp string for our recyclerview
        timeStamp = "$cleanDate $cleanTime"

        //Check that the form is complete before submitting data to the database
        if (!(title.isEmpty() || description.isEmpty() || timeStamp.isEmpty() || drawableSelected == 0)) {
            val habit = Habit(0, title, description, timeStamp, drawableSelected)

            //add the habit if all the fields are filled
            habitViewModel.addHabit(habit)
            Toast.makeText(context, "Habit created successfully!", Toast.LENGTH_SHORT).show()

            //navigate back to our home fragment
            findNavController().navigate(R.id.action_createHabitItem_to_habitList)
        } else {
            Toast.makeText(context, "Please fill all the fields", Toast.LENGTH_SHORT).show()
        }
    }

    :

6. 生活習慣項目のRecyclerView表示

MVVM Habit Tracker App Tutorial in Android Studio (Displaying habits in a RecyclerView)

We're almost done with the app! In this video I'll be showing you how we can show the habits we created inside a RecyclerView, and then in the next video we will take a look at updating these habits.

  1. updateHabitItemに引数を追加
  2. RecyclerView.Adapterの追加
  3. HabitListの実装
    (https://youtu.be/WVWeP_GYNSo?t=505)

updateHabitItemに引数を追加

→ Displaying habits in a RecyclerView (20秒)

main_nav.xmlで、updateHabitItemfragment)にselectedHabitargument)を追加します。

RecyclerView.Adapterの追加

→ Displaying habits in a RecyclerView (58秒)

Adapterを通じて、データ(リスト)をバインドしたり、引数付きで編集画面へ遷移したりします。

ui/fragments/habitlist/adapters/HabitListAdapter.kt

class HabitListAdapter : RecyclerView.Adapter<HabitListAdapter.MyViewHolder>() {

    var habitsList = emptyList<Habit>()

    inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

        init {
            itemView.cv_cardView.setOnClickListener {
                val position = adapterPosition
                Log.d("HabitsListAdapter", "Item clicked at: $position")

                val action =
                    HabitListDirections.actionHabitListToUpdateHabitItem(habitsList[position])
                itemView.findNavController().navigate(action)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        return MyViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.recycler_habit_item, parent,false))
    }

    //todo: initialise the recycler view and set it up to show data (part2)
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val currentHabit = habitsList[position]
        holder.itemView.iv_habit_icon.setImageResource(currentHabit.imageId)
        holder.itemView.tv_item_description.text = currentHabit.habit_description
        holder.itemView.tv_timeElapsed.text =
            Calculations.calculateTimeBetweenDates(currentHabit.habit_startTime)
        holder.itemView.tv_item_createdTimeStamp.text = "Since: ${currentHabit.habit_startTime}"
        holder.itemView.tv_item_title.text = "${currentHabit.habit_title}"
    }

    override fun getItemCount(): Int {
        return habitsList.size
    }

    fun setData(habit: List<Habit>) {
        this.habitsList = habit
        notifyDataSetChanged()
    }

}

HabitListの実装

→ Displaying habits in a RecyclerView (8分29秒)

  • RecyclerViewにアダプタを適用
  • ViewModelを取得し、getAllHabitsにオブザーバーを設定(viewModels()
    getAllHabitsが変化したらアダプタにそのHabitリストを設定する)
  • メニュー項目の実装(全削除機能)
ui/fragments/habitlist/HabitList.kt
class HabitList : Fragment(R.layout.fragment_habit_list) {

    private lateinit var habitList: List<Habit>
    private lateinit var habitViewModel: HabitViewModel
    private lateinit var adapter: HabitListAdapter

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

        adapter = HabitListAdapter()
        rv_habits.adapter = adapter
        rv_habits.layoutManager = LinearLayoutManager(context)

        //Instantiate and create viewmodel observers
        viewModels()

        fab_add.setOnClickListener {
            findNavController().navigate(R.id.action_habitList_to_createHabitItem)
        }

        //Show the options menu in this fragment
        setHasOptionsMenu(true)

        swipeToRefresh.setOnRefreshListener {
            adapter.setData(habitList)
            swipeToRefresh.isRefreshing = false
        }
    }

    private fun viewModels() {
        habitViewModel = ViewModelProvider(this).get(HabitViewModel::class.java)

        habitViewModel.getAllHabits.observe(viewLifecycleOwner, Observer {
            adapter.setData(it)
            habitList = it

            if (it.isEmpty()) {
                rv_habits.visibility = View.GONE
                tv_emptyView.visibility = View.VISIBLE
            } else {
                rv_habits.visibility = View.VISIBLE
                tv_emptyView.visibility = View.GONE
            }
        })
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.nav_menu, menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            R.id.nav_delete -> habitViewModel.deleteAllHabits()
        }
        return super.onOptionsItemSelected(item)
    }

}

7. 生活習慣項目の更新

MVVM Habit Tracker App Tutorial in Android Studio (Updating habits)

In this video I'll show you how we can update our habits in this MVVM app.

CreateHabitItem画面と同様にUpdateHabitItemを実装しますが、次の点が異なります。

  1. 引数の取得(UpdateHabitItemArgs
    → Updating habits (1分3秒)
  2. 削除メニュー(deleteHabit()
    → Updating habits (4分30秒)
  3. 既存項目の表示と更新処理
    → Updating habits (5分51秒)
ui/fragments/updatehabit/UpdateHabitItem.kt
class UpdateHabitItem : Fragment(R.layout.fragment_update_habit_item),
    TimePickerDialog.OnTimeSetListener, DatePickerDialog.OnDateSetListener {
    
    :

    private val args by navArgs<UpdateHabitItemArgs>()
    
    :
    
    @RequiresApi(Build.VERSION_CODES.N)
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        :
        
        //Retrieve data from our habit list
        et_habitTitle_update.setText(args.selectedHabit.habit_title)
        et_habitDescription_update.setText(args.selectedHabit.habit_description)

        :

        //Confirm changes and update the selected item
        btn_confirm_update.setOnClickListener {
            updateHabit()
        }

        //Show the options menu in this fragment
        setHasOptionsMenu(true)
    }

    private fun updateHabit() {
        //Get text from editTexts
        title = et_habitTitle_update.text.toString()
        description = et_habitDescription_update.text.toString()

        //Create a timestamp string for our recyclerview
        timeStamp = "$cleanDate $cleanTime"

        //Check that the form is complete before submitting data to the database
        if (!(title.isEmpty() || description.isEmpty() || timeStamp.isEmpty() || drawableSelected == 0)) {
            val habit =
                Habit(args.selectedHabit.id, title, description, timeStamp, drawableSelected)

            //add the habit if all the fields are filled
            habitViewModel.updateHabit(habit)
            Toast.makeText(context, "Habit updated! successfully!", Toast.LENGTH_SHORT).show()

            //navigate back to our home fragment
            findNavController().navigate(R.id.action_updateHabitItem_to_habitList)
        } else {
            Toast.makeText(context, "Please fill all the fields", Toast.LENGTH_SHORT).show()
        }
    }

    :
    
    //Create options menu
    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.single_item_menu, menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            R.id.nav_delete -> {
                deleteHabit(args.selectedHabit)
            }
        }
        return super.onOptionsItemSelected(item)
    }
    //------------------------------------------

    //Delete a single Habit
    private fun deleteHabit(habit: Habit) {
        habitViewModel.deleteHabit(habit)
        Toast.makeText(context, "Habit successfully deleted!", Toast.LENGTH_SHORT).show()

        findNavController().navigate(R.id.action_updateHabitItem_to_habitList)
    }
    //------------------------------------------

}

尚、チュートリアルでは、既存項目の表示は、タイトルと説明のみとなっており、更新画面を開いたときに、日時やアイコンは未選択状態です。

8. ViewPager2によるイントロ表示

MVVM Habit Tracker App Tutorial in Android Studio (ViewPager2 Intro Screen)

Well done on making it this far! In this final tutorial, we'll be creating the intro screen for this habit tracking app.

  1. アクテビティの追加(IntroActivity)
    → ViewPager2 Intro Screen (0分22秒)
  2. アダプタの追加(ViewPagerIntroAdapter
    → ViewPager2 Intro Screen (4分34秒)
  3. アクテビティの実装(IntroActivity)
    → ViewPager2 Intro Screen (7分25秒)
  4. ボタンのアニメーション表示
    → ViewPager2 Intro Screen (9分28秒)
  5. イントロ画面リストの追加
    → ViewPager2 Intro Screen (10分50秒)
  6. インジケータの表示
    → ViewPager2 Intro Screen (12分32秒)

ViewPager2を使って、3つのイントロ画面をスクロール表示します。

おわりに

チュートリアル動画を紹介しました。
androidx.navigation、androidx.room、androidx.viewpager2をおりまぜながら、MVVMアーキテクチャによるAndroidアプリの開発をこの動画で学習することができます。

チュートリアル動画

また、公式のCodelabページで、各アーキテクチャコンポーネントについて学習することができます。

[Codelab] Jetpack Navigation

[Codelab] Android Room とビュー - Kotlin

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?