MVVM - Kotlin Android Studio
MVVMアーキテクチャ
- 「Model」(モデル)
- 「View」(ビュー)
- 「ViewModel」(ビューモデル)
Observerによる表示の更新
- ViewModel
- LiveData
- Observer
Room、ViewModel、LiveData を使用したアプリの設計・構築方法
チュートリアル - 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
- app/build.gradle ファイルの編集
- build.gradle ファイルの編集
- Fragment(blank) の追加(リスト表示、追加、更新)と設定(画面遷移)
- アイコンの追加と設定(res/drawable)
- メニューバーの追加と設定(res/menu)
- レイアウトの追加と設定(res/layout)
GitHub リポジトリ
GitHub: https://github.com/indently/Habit_Tracker_2020
app/build.gradle
ファイルの編集
プラグインの追加
apply plugin: "kotlin-kapt"
apply plugin: "androidx.navigation.safeargs.kotlin"
コンパイルオプションの設定
// Compile options
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
依存モジュールの追加
- Coroutines
- Navigation Components
- Lifecycle Components
- Retrofit2
- Room Components
- ViewPager2
- Circle Indicator (ViewPager2)
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
ファイルの編集
classpathの追加
def nav_version = "2.3.0"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
Gradle Sync(再同期)
Sync Project with Gradle Files
アイコンで、プラグインを再同期します。
カラーの定義
app/src/main/res/values/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
main_nav.xml
ファイルで、fragment
を追加し、それぞれのリンクを定義します。
- habitList
- createHabitItem
- updateHabitItem
drawable - app/src/main/res/drawable
vector
やselector
を定義します。
- ハンバーガーアイコン(セレクターあり)
- ティカップアイコン(セレクターあり)
- 禁煙アイコン(セレクターあり)
- 削除アイコン
- 追加アイコン
- ラウンドボタン(
shape
)
menu - app/src/main/res/menu
- 全削除アイテム(
nav_main.xml
) - 削除アイテム(
single_item_menu.xml
)
layout - app/src/main/res/layout
-
activity_main.xml
にNavHostFragment
をアタッチメント -
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.
- Packageの追加
- fragmentsパッケージの編集
- データクラスの追加
- データアクセスオブジェクトの追加
Packageの追加(フォルダ構造)
→ Room Database & ViewModel (10秒)
-
ui
、logic
、data
パッケージ(フォルダ)を追加 -
ui
内にfragments
パッケージを追加 -
ui/fragments
内にcreatehabit
、habitlist
、updatehabit
パッケージを追加
(ktファイルを各パッケージに移動) -
ui
内にviewmodels
とintroscreen
パッケージを追加 -
logic
内にdao
、repository
、utils
パッケージを追加 -
data
内にdatabase
とmodels
パッケージを追加
.
├─ [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秒)
@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 class IntroView(val description: String, val image: Int) {
}
データアクセスオブジェクトの追加(logic.dao
)
HabitDaoインターフェース(DAO)
→ Room Database & ViewModel (5分38秒)
@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秒)
@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秒)
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秒)
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()
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
クラスで、設定します。
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.
画面遷移 - リスト画面から追加画面へ
リスト画面の追加ボタン(fab_add
)で、リスト画面から追加画面へ遷移します(R.id.action_habitList_to_createHabitItem
)。
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)
}
:
}
:
}
追加画面の実装
- 日付選択リスナーと時刻選択リスナー
- 内部変数(項目値)の初期化
- ViewModelの取得(
HabitViewModel
) - 選択アイコンの実装
- 日付選択と時刻選択の実装
- 登録ボタンの実装
日付選択リスナーと時刻選択リスナー
TimePickerDialog.OnTimeSetListener
インターフェースとDatePickerDialog.OnDateSetListener
インターフェースをCreateHabitItem
クラスで実装します。
内部変数(項目値)の初期化
項目の入力値を一時保持する内部変数を初期化します。
永続化項目
- title
- description
- drawableSelected
- timeStamp
現在日付
- day
- month
- year
現在時刻
- hour
- minute
選択日付・選択時刻
- cleanDate
- cleanTime
ViewModelの取得(HabitViewModel
)
HabitViewModel
インスタンスを取得し、内部保持します。データベースへの登録に使用します(habitViewModel.addHabit()
)。
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)
:
}
:
}
選択アイコンの実装
選択アイコンの選択状態を切り替える処理を実装し、選択されたアイコンを内部保持します(drawableSelected
)。
:
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
}
}
:
日付選択と時刻選択の実装
- 現在日時の取得 -
getTimeCalendar()
,getDateCalendar()
- リスナーの実装 -
onTimeSet()
,onDateSet()
- 日時選択ボタン -
pickDateAndTime()
:
@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
でデータベースへ追加)
- 登録ボタンのイベントハンドラ -
btn_confirm.setOnClickListener {...}
- 入力値を内部変数に代入
- 入力検証
-
HabitViewModel
インスタンスでデータベースへ追加 - リスト画面へ遷移 -
R.id.action_createHabitItem_to_habitList
:
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.
-
updateHabitItem
に引数を追加 -
RecyclerView.Adapter
の追加 -
HabitList
の実装
(https://youtu.be/WVWeP_GYNSo?t=505)
updateHabitItem
に引数を追加
→ Displaying habits in a RecyclerView (20秒)
main_nav.xml
で、updateHabitItem
(fragment
)にselectedHabit
(argument
)を追加します。
RecyclerView.Adapter
の追加
→ Displaying habits in a RecyclerView (58秒)
Adapterを通じて、データ(リスト)をバインドしたり、引数付きで編集画面へ遷移したりします。
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リストを設定する) - メニュー項目の実装(全削除機能)
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
を実装しますが、次の点が異なります。
- 引数の取得(
UpdateHabitItemArgs
)
→ Updating habits (1分3秒) - 削除メニュー(
deleteHabit()
)
→ Updating habits (4分30秒) - 既存項目の表示と更新処理
→ Updating habits (5分51秒)
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.
- アクテビティの追加(
IntroActivity
)
→ ViewPager2 Intro Screen (0分22秒) - アダプタの追加(
ViewPagerIntroAdapter
)
→ ViewPager2 Intro Screen (4分34秒) - アクテビティの実装(
IntroActivity
)
→ ViewPager2 Intro Screen (7分25秒) - ボタンのアニメーション表示
→ ViewPager2 Intro Screen (9分28秒) - イントロ画面リストの追加
→ ViewPager2 Intro Screen (10分50秒) - インジケータの表示
→ ViewPager2 Intro Screen (12分32秒)
ViewPager2を使って、3つのイントロ画面をスクロール表示します。
おわりに
チュートリアル動画を紹介しました。
androidx.navigation、androidx.room、androidx.viewpager2をおりまぜながら、MVVMアーキテクチャによるAndroidアプリの開発をこの動画で学習することができます。
チュートリアル動画
また、公式のCodelabページで、各アーキテクチャコンポーネントについて学習することができます。
[Codelab] Jetpack Navigation
[Codelab] Android Room とビュー - Kotlin