Skip to content

Commit 0e69247

Browse files
authored
Merge pull request #17 from mrtry/feature/edit-task
Feature/edit task
2 parents 20f9fdc + b43acd1 commit 0e69247

33 files changed

+654
-25
lines changed

app/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ dependencies {
6363
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
6464
implementation 'androidx.core:core-ktx:1.3.2'
6565
implementation 'androidx.appcompat:appcompat:1.2.0'
66+
implementation "androidx.activity:activity-ktx:1.1.0"
67+
implementation "androidx.fragment:fragment-ktx:1.2.5"
6668
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
6769
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
6870
implementation 'com.google.android.material:material:1.2.1'

app/src/main/java/io/github/mrtry/todolist/MainApplication.kt

+8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import android.app.Application
44
import android.content.Context
55
import io.github.mrtry.todolist.di.component.AppComponent
66
import io.github.mrtry.todolist.di.component.DaggerAppComponent
7+
import io.github.mrtry.todolist.misc.ui.WholeActivityDelegate
78
import timber.log.Timber
9+
import javax.inject.Inject
810

911

1012
class MainApplication : Application() {
@@ -14,6 +16,9 @@ class MainApplication : Application() {
1416
}
1517
}
1618

19+
@Inject
20+
lateinit var wholeActivityDelegate: WholeActivityDelegate
21+
1722
private val appComponent: AppComponent by lazy {
1823
DaggerAppComponent.builder().build().also {
1924
it.inject(this)
@@ -23,5 +28,8 @@ class MainApplication : Application() {
2328
override fun onCreate() {
2429
super.onCreate()
2530
Timber.plant(Timber.DebugTree())
31+
32+
appComponent
33+
registerActivityLifecycleCallbacks(wholeActivityDelegate)
2634
}
2735
}

app/src/main/java/io/github/mrtry/todolist/app/splash/ui/navigator/SplashNavigator.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package io.github.mrtry.todolist.app.splash.ui.navigator
22

3-
import android.app.Activity
3+
import androidx.appcompat.app.AppCompatActivity
44
import com.firebase.ui.auth.AuthUI
55
import io.github.mrtry.todolist.app.splash.ui.result.SplashActivityResult
66
import io.github.mrtry.todolist.app.todo.ui.ToDoActivity
@@ -11,7 +11,7 @@ import javax.inject.Inject
1111
@ActivityScope
1212
class SplashNavigator
1313
@Inject constructor(
14-
private val activity: Activity
14+
private val activity: AppCompatActivity
1515
) : AbsNavigator(activity) {
1616
fun navigateToToDo() {
1717
val intent = ToDoActivity.createIntent(activity)

app/src/main/java/io/github/mrtry/todolist/app/splash/viewmodel/SplashViewModel.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ package io.github.mrtry.todolist.app.splash.viewmodel
33
import io.github.mrtry.todolist.app.splash.ui.navigator.SplashNavigator
44
import io.github.mrtry.todolist.auth.repository.AccountRepository
55
import io.github.mrtry.todolist.di.scope.ActivityScope
6+
import io.github.mrtry.todolist.misc.ui.viewmodel.ViewModel
67
import javax.inject.Inject
78

89
@ActivityScope
910
class SplashViewModel
1011
@Inject constructor(
1112
private val authenticationClient: AccountRepository,
1213
private val navigator: SplashNavigator
13-
) {
14+
) : ViewModel {
1415
fun ensuredLoggedIn() {
1516
when (authenticationClient.isLoggedIn()) {
1617
true -> navigator.navigateToToDo()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package io.github.mrtry.todolist.app.todo.ui
2+
3+
import android.os.Bundle
4+
import android.view.KeyEvent
5+
import android.view.LayoutInflater
6+
import android.view.View
7+
import android.view.ViewGroup
8+
import androidx.activity.addCallback
9+
import androidx.fragment.app.DialogFragment
10+
import androidx.fragment.app.Fragment
11+
import io.github.mrtry.todolist.R
12+
import io.github.mrtry.todolist.app.todo.ui.navigator.EditTaskNavigator
13+
import io.github.mrtry.todolist.app.todo.viewmodel.EditTaskViewModel
14+
import io.github.mrtry.todolist.databinding.FragmentEditTaskBinding
15+
import io.github.mrtry.todolist.di.Injectable
16+
import io.github.mrtry.todolist.di.component.EditTaskComponent
17+
import io.github.mrtry.todolist.di.component.ToDoComponent
18+
import io.github.mrtry.todolist.di.module.FragmentModule
19+
import io.github.mrtry.todolist.di.scope.FragmentScope
20+
import io.github.mrtry.todolist.di.utils.ComponentUtils
21+
import io.github.mrtry.todolist.misc.extension.observeNonNull
22+
import io.github.mrtry.todolist.misc.ui.binding.Bindable
23+
import io.github.mrtry.todolist.task.entity.Task
24+
import kotlinx.coroutines.CoroutineScope
25+
import kotlinx.coroutines.cancelChildren
26+
import javax.inject.Inject
27+
28+
29+
private const val KEY_TASK = "KEY_TASK"
30+
31+
class EditTaskDialogFragment : DialogFragment(), Injectable<EditTaskComponent>, Bindable<FragmentEditTaskBinding> {
32+
companion object {
33+
val TAG: String = EditTaskDialogFragment::class.java.simpleName
34+
35+
fun newInstance(task: Task): EditTaskDialogFragment {
36+
val fragment = EditTaskDialogFragment()
37+
val args = Bundle()
38+
args.putParcelable(KEY_TASK, task)
39+
fragment.arguments = args
40+
return fragment
41+
}
42+
}
43+
44+
override val viewBinding: FragmentEditTaskBinding by lazy {
45+
FragmentEditTaskBinding.inflate(layoutInflater)
46+
}
47+
48+
override lateinit var component: EditTaskComponent
49+
50+
@Inject
51+
lateinit var viewModel: EditTaskViewModel
52+
53+
@Inject
54+
lateinit var navigator: EditTaskNavigator
55+
56+
@Inject
57+
lateinit var coroutineScope: CoroutineScope
58+
59+
override fun onCreate(savedInstanceState: Bundle?) {
60+
super.onCreate(savedInstanceState)
61+
setStyle(STYLE_NORMAL, R.style.Widget_FullScreenDialog)
62+
}
63+
64+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
65+
super.onCreateView(inflater, container, savedInstanceState)
66+
return viewBinding.root
67+
}
68+
69+
override fun onActivityCreated(savedInstanceState: Bundle?) {
70+
super.onActivityCreated(savedInstanceState)
71+
component = ComponentUtils.getComponent<ToDoComponent>(requireActivity())
72+
.plusEditTaskComponent(FragmentModule(this))
73+
component.inject(this)
74+
75+
savedInstanceState?.let {
76+
viewModel.onRestoreInstanceState(it)
77+
}
78+
79+
with(viewBinding) {
80+
viewModel = this@EditTaskDialogFragment.viewModel
81+
lifecycleOwner = this@EditTaskDialogFragment
82+
}
83+
84+
requireActivity().onBackPressedDispatcher.addCallback(this) {
85+
navigator.showSavingAlert()
86+
}
87+
88+
// DialogFragmentのcancel()を無効にして、back pressをハンドリングする
89+
// see: https://stackoverflow.com/a/7622065
90+
isCancelable = false
91+
dialog?.setOnKeyListener { _, keyCode, event ->
92+
if (keyCode == KeyEvent.KEYCODE_BACK && event.action === KeyEvent.ACTION_UP) {
93+
navigator.onBackPressed()
94+
true
95+
} else false
96+
}
97+
98+
viewModel.title.observeNonNull(this@EditTaskDialogFragment) {
99+
with(viewBinding.toolbar) {
100+
menu.getItem(0).isEnabled = it.isNotEmpty()
101+
invalidate()
102+
}
103+
}
104+
}
105+
106+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
107+
super.onViewCreated(view, savedInstanceState)
108+
with(viewBinding.toolbar) {
109+
setNavigationOnClickListener {
110+
navigator.onBackPressed()
111+
}
112+
113+
inflateMenu(R.menu.menu_fragment_edit_task)
114+
setOnMenuItemClickListener {
115+
this@EditTaskDialogFragment.viewModel.onSaveClick()
116+
true
117+
}
118+
}
119+
}
120+
121+
override fun onStart() {
122+
super.onStart()
123+
dialog?.window?.setLayout(
124+
ViewGroup.LayoutParams.MATCH_PARENT,
125+
ViewGroup.LayoutParams.MATCH_PARENT
126+
)
127+
}
128+
129+
override fun onSaveInstanceState(outState: Bundle) {
130+
super.onSaveInstanceState(outState)
131+
viewModel.onSaveInstanceState(outState)
132+
}
133+
134+
override fun onStop() {
135+
super.onStop()
136+
coroutineScope.coroutineContext.cancelChildren()
137+
}
138+
}
139+
140+
@FragmentScope
141+
class EditTaskDialogFragmentValueHolder
142+
@Inject constructor(fragment: Fragment) {
143+
val task: Task = fragment.requireArguments().getParcelable(KEY_TASK)!!
144+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.github.mrtry.todolist.app.todo.ui.navigator
2+
3+
import androidx.appcompat.app.AppCompatActivity
4+
import androidx.fragment.app.DialogFragment
5+
import androidx.fragment.app.Fragment
6+
import com.google.android.material.snackbar.Snackbar
7+
import io.github.mrtry.todolist.R
8+
import io.github.mrtry.todolist.databinding.FragmentEditTaskBinding
9+
import io.github.mrtry.todolist.di.scope.FragmentScope
10+
import io.github.mrtry.todolist.misc.ui.navigator.AbsFragmentNavigator
11+
import javax.inject.Inject
12+
13+
@FragmentScope
14+
class EditTaskNavigator
15+
@Inject constructor(
16+
activity: AppCompatActivity,
17+
private val fragment: Fragment
18+
) : AbsFragmentNavigator(activity, fragment) {
19+
fun dismissDialog() {
20+
(fragment as DialogFragment).dismiss()
21+
}
22+
23+
fun showSavingAlert() {
24+
showAlert(
25+
R.string.edit_task_fragment_alert_confirm,
26+
R.string.edit_task_fragment_alert_label_positive,
27+
{ _, _ -> dismissDialog() }
28+
)
29+
}
30+
31+
override fun showSnackBar(messageId: Int, length: Int) {
32+
Snackbar.make(getFragmentBindingAs<FragmentEditTaskBinding>().container, messageId, length).show()
33+
}
34+
}

app/src/main/java/io/github/mrtry/todolist/app/todo/ui/navigator/ToDoNavigator.kt

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
package io.github.mrtry.todolist.app.todo.ui.navigator
22

3-
import android.app.Activity
3+
import androidx.appcompat.app.AppCompatActivity
44
import io.github.mrtry.todolist.R
55
import io.github.mrtry.todolist.app.splash.ui.SplashActivity
6+
import io.github.mrtry.todolist.app.todo.ui.EditTaskDialogFragment
67
import io.github.mrtry.todolist.di.scope.ActivityScope
78
import io.github.mrtry.todolist.misc.ui.navigator.AbsNavigator
9+
import io.github.mrtry.todolist.task.entity.Task
810
import javax.inject.Inject
911

1012
@ActivityScope
1113
class ToDoNavigator
1214
@Inject constructor(
13-
private val activity: Activity
15+
private val activity: AppCompatActivity
1416
) : AbsNavigator(activity) {
1517
fun navigateToSplash() {
1618
val intent = SplashActivity.createIntent(activity)
@@ -26,4 +28,9 @@ class ToDoNavigator
2628
android.R.string.cancel
2729
)
2830
}
31+
32+
fun showEditTask(task: Task) {
33+
val fragmentDialog = EditTaskDialogFragment.newInstance(task)
34+
fragmentDialog.show(activity.supportFragmentManager, EditTaskDialogFragment.TAG)
35+
}
2936
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package io.github.mrtry.todolist.app.todo.viewmodel
2+
3+
import android.os.Bundle
4+
import androidx.lifecycle.MutableLiveData
5+
import io.github.mrtry.todolist.R
6+
import io.github.mrtry.todolist.app.todo.ui.EditTaskDialogFragmentValueHolder
7+
import io.github.mrtry.todolist.app.todo.ui.navigator.EditTaskNavigator
8+
import io.github.mrtry.todolist.di.scope.FragmentScope
9+
import io.github.mrtry.todolist.misc.extension.requireValue
10+
import io.github.mrtry.todolist.misc.ui.viewmodel.ViewModel
11+
import io.github.mrtry.todolist.task.domainservice.TaskDomainService
12+
import kotlinx.coroutines.CancellationException
13+
import kotlinx.coroutines.CoroutineScope
14+
import kotlinx.coroutines.launch
15+
import timber.log.Timber
16+
import javax.inject.Inject
17+
18+
private const val KEY_TITLE = "KEY_TITLE"
19+
private const val KEY_DESCRIPTION = "KEY_DESCRIPTION"
20+
21+
@FragmentScope
22+
class EditTaskViewModel
23+
@Inject constructor(
24+
private val domainService: TaskDomainService,
25+
private val navigator: EditTaskNavigator,
26+
private val valueHolder: EditTaskDialogFragmentValueHolder,
27+
private val coroutineScope: CoroutineScope
28+
) : ViewModel {
29+
val title: MutableLiveData<String> = MutableLiveData(valueHolder.task.title)
30+
val description: MutableLiveData<String> = MutableLiveData(valueHolder.task.description)
31+
32+
val isSaving: MutableLiveData<Boolean> = MutableLiveData(false)
33+
34+
override fun onSaveInstanceState(outState: Bundle) {
35+
with(outState) {
36+
putString(KEY_TITLE, title.requireValue())
37+
putString(KEY_DESCRIPTION, description.requireValue())
38+
}
39+
}
40+
41+
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
42+
with(savedInstanceState) {
43+
getString(KEY_TITLE).also {
44+
title.value = it
45+
}
46+
getString(KEY_DESCRIPTION).also {
47+
description.value = it
48+
}
49+
}
50+
}
51+
52+
fun onSaveClick() {
53+
val task = valueHolder.task.copy(
54+
title = title.requireValue(),
55+
description = description.requireValue()
56+
)
57+
58+
coroutineScope.launch {
59+
try {
60+
isSaving.value = true
61+
domainService.saveToRepository(task)
62+
navigator.dismissDialog()
63+
} catch (e: Exception) {
64+
if (e is CancellationException) return@launch
65+
66+
Timber.e(e)
67+
navigator.showSnackBar(R.string.edit_task_fragment_error_update_task_failed)
68+
} finally {
69+
isSaving.value = false
70+
}
71+
}
72+
}
73+
}

app/src/main/java/io/github/mrtry/todolist/app/todo/viewmodel/TaskViewModel.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import io.github.mrtry.todolist.R
99
import io.github.mrtry.todolist.app.todo.ui.navigator.ToDoNavigator
1010
import io.github.mrtry.todolist.di.scope.ActivityScope
1111
import io.github.mrtry.todolist.misc.extension.requireValue
12+
import io.github.mrtry.todolist.misc.ui.viewmodel.ViewModel
1213
import io.github.mrtry.todolist.task.domainservice.TaskDomainService
1314
import io.github.mrtry.todolist.task.entity.Task
1415
import kotlinx.coroutines.CancellationException
@@ -23,7 +24,7 @@ class TaskViewModel
2324
private val navigator: ToDoNavigator,
2425
private val domainService: TaskDomainService,
2526
private val coroutineScope: CoroutineScope
26-
) : TextView.OnEditorActionListener {
27+
) : ViewModel, TextView.OnEditorActionListener {
2728
val taskName: MutableLiveData<String> = MutableLiveData("")
2829
val isSaving: MutableLiveData<Boolean> = MutableLiveData(false)
2930

0 commit comments

Comments
 (0)