Skip to content

Commit e7b9385

Browse files
authored
Merge pull request #12 from mrtry/feature/todo
Feature/todo
2 parents 795b9d6 + b78d21b commit e7b9385

36 files changed

+1037
-48
lines changed

app/build.gradle

+16-6
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,22 @@ android {
4949
unitTests {
5050
includeAndroidResources = true
5151
}
52+
53+
unitTests.all {
54+
testLogging {
55+
events 'started', 'passed', 'skipped', 'failed'
56+
}
57+
}
5258
}
5359
}
5460

5561
dependencies {
5662
implementation fileTree(dir: "libs", include: ["*.jar"])
5763
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
58-
implementation 'androidx.core:core-ktx:1.3.1'
64+
implementation 'androidx.core:core-ktx:1.3.2'
5965
implementation 'androidx.appcompat:appcompat:1.2.0'
60-
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
61-
testImplementation 'junit:junit:4.12'
62-
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
63-
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
66+
implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
67+
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
6468
implementation 'com.google.android.material:material:1.2.1'
6569

6670
// dagger
@@ -72,24 +76,30 @@ dependencies {
7276
def coroutine_version = "1.3.9"
7377
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
7478
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutine_version"
79+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutine_version"
7580

7681
// deploy gate
7782
implementation 'com.deploygate:sdk:4.1.0'
7883

7984
// Firebase
8085
implementation 'com.google.firebase:firebase-analytics:17.5.0'
8186
implementation 'com.google.firebase:firebase-auth:19.4.0'
87+
implementation 'com.google.firebase:firebase-firestore:21.7.0'
8288
implementation 'com.firebaseui:firebase-ui-auth:6.2.0'
8389

8490
// test
8591
testImplementation 'junit:junit:4.12'
8692
testImplementation 'org.robolectric:robolectric:4.4'
8793
testImplementation 'androidx.test:core:1.3.0'
94+
testImplementation "androidx.arch.core:core-testing:2.1.0"
8895
testImplementation 'org.mockito:mockito-core:3.3.3'
8996
testImplementation 'org.mockito:mockito-android:3.3.3'
9097
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
91-
androidTestImplementation 'androidx.annotation:annotation:1.1.0'
98+
testImplementation "com.google.truth:truth:1.0.1"
99+
testImplementation 'androidx.test.ext:junit:1.1.2'
92100
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
101+
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
102+
androidTestImplementation 'androidx.annotation:annotation:1.1.0'
93103

94104
// other
95105
implementation 'com.jakewharton.timber:timber:4.7.1'

app/src/main/AndroidManifest.xml

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
<category android:name="android.intent.category.LAUNCHER" />
1717
</intent-filter>
1818
</activity>
19+
20+
<activity android:name=".app.todo.ui.ToDoActivity" />
1921
</application>
2022

2123
</manifest>

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

-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import io.github.mrtry.todolist.di.Injectable
1414
import io.github.mrtry.todolist.di.component.LoginComponent
1515
import io.github.mrtry.todolist.di.module.ActivityModule
1616
import io.github.mrtry.todolist.misc.ui.binding.Bindable
17-
import kotlinx.coroutines.CoroutineScope
1817
import javax.inject.Inject
1918

2019
class SplashActivity : AppCompatActivity(), Injectable<LoginComponent>, Bindable<ActivitySplashBinding> {
@@ -25,9 +24,6 @@ class SplashActivity : AppCompatActivity(), Injectable<LoginComponent>, Bindable
2524
@Inject
2625
internal lateinit var navigator: SplashNavigator
2726

28-
@Inject
29-
internal lateinit var coroutineScope: CoroutineScope
30-
3127
override val viewBinding: ActivitySplashBinding by lazy {
3228
DataBindingUtil.setContentView<ActivitySplashBinding>(this, R.layout.activity_splash)
3329
}

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package io.github.mrtry.todolist.app.splash.ui.navigator
33
import android.app.Activity
44
import com.firebase.ui.auth.AuthUI
55
import io.github.mrtry.todolist.app.splash.ui.result.SplashActivityResult
6+
import io.github.mrtry.todolist.app.todo.ui.ToDoActivity
67
import io.github.mrtry.todolist.di.scope.ActivityScope
78
import io.github.mrtry.todolist.misc.ui.navigator.AbsNavigator
8-
import timber.log.Timber
99
import javax.inject.Inject
1010

1111
@ActivityScope
@@ -14,8 +14,9 @@ class SplashNavigator
1414
private val activity: Activity
1515
) : AbsNavigator(activity) {
1616
fun navigateToToDo() {
17-
// TODO
18-
Timber.d("navigateToToDo() called")
17+
val intent = ToDoActivity.createIntent(activity)
18+
activity.startActivity(intent)
19+
finishCurrentActivity()
1920
}
2021

2122
fun navigateToAuthentication() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.github.mrtry.todolist.app.todo.ui
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import android.os.Bundle
6+
import androidx.appcompat.app.AppCompatActivity
7+
import androidx.databinding.DataBindingUtil
8+
import androidx.recyclerview.widget.LinearLayoutManager
9+
import io.github.mrtry.todolist.MainApplication
10+
import io.github.mrtry.todolist.R
11+
import io.github.mrtry.todolist.app.todo.ui.adapter.ToDoAdapter
12+
import io.github.mrtry.todolist.app.todo.ui.navigator.ToDoNavigator
13+
import io.github.mrtry.todolist.app.todo.viewmodel.ToDoViewModel
14+
import io.github.mrtry.todolist.databinding.ActivityToDoBinding
15+
import io.github.mrtry.todolist.di.Injectable
16+
import io.github.mrtry.todolist.di.component.ToDoComponent
17+
import io.github.mrtry.todolist.di.module.ActivityModule
18+
import io.github.mrtry.todolist.misc.ui.binding.Bindable
19+
import kotlinx.coroutines.CoroutineScope
20+
import kotlinx.coroutines.cancelChildren
21+
import javax.inject.Inject
22+
23+
class ToDoActivity : AppCompatActivity(), Injectable<ToDoComponent>, Bindable<ActivityToDoBinding> {
24+
companion object {
25+
fun createIntent(context: Context): Intent =
26+
Intent(context, ToDoActivity::class.java)
27+
}
28+
29+
@Inject
30+
internal lateinit var viewModel: ToDoViewModel
31+
32+
@Inject
33+
internal lateinit var navigator: ToDoNavigator
34+
35+
@Inject
36+
internal lateinit var coroutineScope: CoroutineScope
37+
38+
override val viewBinding: ActivityToDoBinding by lazy {
39+
DataBindingUtil.setContentView<ActivityToDoBinding>(this, R.layout.activity_to_do)
40+
}
41+
42+
override val component: ToDoComponent by lazy {
43+
MainApplication.getComponent(this)
44+
.plusToDoComponent(ActivityModule(this))
45+
}
46+
47+
override fun onCreate(savedInstanceState: Bundle?) {
48+
super.onCreate(savedInstanceState)
49+
component.inject(this)
50+
setSupportActionBar(viewBinding.toolbar)
51+
52+
with(viewBinding) {
53+
viewModel = this@ToDoActivity.viewModel
54+
lifecycleOwner = this@ToDoActivity
55+
56+
with(bannerAddTask) {
57+
viewModel = this@ToDoActivity.viewModel.taskViewModel
58+
lifecycleOwner = this@ToDoActivity
59+
}
60+
61+
with(list) {
62+
adapter = ToDoAdapter(this@ToDoActivity, this@ToDoActivity.viewModel.items, this@ToDoActivity)
63+
layoutManager = LinearLayoutManager(this@ToDoActivity)
64+
}
65+
}
66+
67+
viewModel.load()
68+
}
69+
70+
override fun onStop() {
71+
super.onStop()
72+
coroutineScope.coroutineContext.cancelChildren()
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.github.mrtry.todolist.app.todo.ui.adapter
2+
3+
import android.content.Context
4+
import android.view.LayoutInflater
5+
import android.view.View
6+
import android.view.ViewGroup
7+
import androidx.databinding.DataBindingUtil
8+
import androidx.databinding.ObservableList
9+
import androidx.lifecycle.LifecycleOwner
10+
import androidx.recyclerview.widget.RecyclerView
11+
import io.github.mrtry.todolist.R
12+
import io.github.mrtry.todolist.app.todo.viewmodel.ToDoListItemViewModel
13+
import io.github.mrtry.todolist.databinding.ListItemToDoBinding
14+
import io.github.mrtry.todolist.misc.extension.addOnListChangedSimpleCallback
15+
16+
class ToDoAdapter(
17+
context: Context,
18+
private val items: ObservableList<ToDoListItemViewModel>,
19+
private val lifecycleOwner: LifecycleOwner
20+
) : RecyclerView.Adapter<ToDoAdapter.ViewHolder>() {
21+
private val inflater = LayoutInflater.from(context)
22+
23+
init {
24+
items.addOnListChangedSimpleCallback {
25+
notifyDataSetChanged()
26+
}
27+
}
28+
29+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
30+
ViewHolder(inflater.inflate(R.layout.list_item_to_do, parent, false))
31+
32+
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
33+
val item = getItem(position)
34+
35+
with(holder.binding) {
36+
lifecycleOwner = this@ToDoAdapter.lifecycleOwner
37+
viewModel = item
38+
executePendingBindings()
39+
}
40+
}
41+
42+
override fun getItemCount(): Int = items.size
43+
44+
private fun getItem(position: Int): ToDoListItemViewModel = items[position]
45+
46+
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
47+
val binding: ListItemToDoBinding = DataBindingUtil.bind(itemView)!!
48+
}
49+
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.github.mrtry.todolist.app.todo.ui.navigator
2+
3+
import android.app.Activity
4+
import io.github.mrtry.todolist.di.scope.ActivityScope
5+
import io.github.mrtry.todolist.misc.ui.navigator.AbsNavigator
6+
import javax.inject.Inject
7+
8+
@ActivityScope
9+
class ToDoNavigator
10+
@Inject constructor(
11+
private val activity: Activity
12+
) : AbsNavigator(activity)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package io.github.mrtry.todolist.app.todo.viewmodel
2+
3+
import android.view.KeyEvent
4+
import android.view.View
5+
import android.view.inputmethod.EditorInfo
6+
import android.widget.TextView
7+
import androidx.lifecycle.MutableLiveData
8+
import io.github.mrtry.todolist.R
9+
import io.github.mrtry.todolist.app.todo.ui.navigator.ToDoNavigator
10+
import io.github.mrtry.todolist.di.scope.ActivityScope
11+
import io.github.mrtry.todolist.misc.extension.requireValue
12+
import io.github.mrtry.todolist.todo.domainservice.ToDoDomainService
13+
import io.github.mrtry.todolist.todo.entity.ToDo
14+
import kotlinx.coroutines.CancellationException
15+
import kotlinx.coroutines.CoroutineScope
16+
import kotlinx.coroutines.launch
17+
import timber.log.Timber
18+
import javax.inject.Inject
19+
20+
@ActivityScope
21+
class TaskViewModel
22+
@Inject constructor(
23+
private val navigator: ToDoNavigator,
24+
private val domainService: ToDoDomainService,
25+
private val coroutineScope: CoroutineScope
26+
) : TextView.OnEditorActionListener {
27+
val taskName: MutableLiveData<String> = MutableLiveData("")
28+
val isSaving: MutableLiveData<Boolean> = MutableLiveData(false)
29+
30+
fun onAddTaskButtonClick(v: View?) {
31+
coroutineScope.launch {
32+
if (taskName.requireValue().isEmpty()) return@launch
33+
34+
isSaving.value = true
35+
val todo = ToDo(title = taskName.value.orEmpty())
36+
37+
try {
38+
domainService.saveToRepository(todo)
39+
taskName.value = ""
40+
} catch (e: Exception) {
41+
if (e is CancellationException) return@launch
42+
43+
Timber.e(e)
44+
navigator.showSnackBar(R.string.to_do_activity_error_add_task_failed)
45+
} finally {
46+
isSaving.value = false
47+
}
48+
}
49+
}
50+
51+
override fun onEditorAction(v: TextView?, actionId: Int, keyEvent: KeyEvent?): Boolean {
52+
if (actionId == EditorInfo.IME_ACTION_DONE) {
53+
onAddTaskButtonClick(v)
54+
}
55+
return false
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package io.github.mrtry.todolist.app.todo.viewmodel
2+
3+
import androidx.lifecycle.LifecycleOwner
4+
import androidx.lifecycle.MutableLiveData
5+
import io.github.mrtry.todolist.R
6+
import io.github.mrtry.todolist.app.todo.ui.navigator.ToDoNavigator
7+
import io.github.mrtry.todolist.misc.extension.observeNonNull
8+
import io.github.mrtry.todolist.misc.extension.requireValue
9+
import io.github.mrtry.todolist.todo.domainservice.ToDoDomainService
10+
import io.github.mrtry.todolist.todo.entity.ToDo
11+
import kotlinx.coroutines.CancellationException
12+
import kotlinx.coroutines.CoroutineScope
13+
import kotlinx.coroutines.launch
14+
import timber.log.Timber
15+
16+
class ToDoListItemViewModel(
17+
todo: ToDo,
18+
lifecycleOwner: LifecycleOwner,
19+
private val navigator: ToDoNavigator,
20+
private val domainService: ToDoDomainService,
21+
private val coroutineScope: CoroutineScope
22+
) {
23+
val todo: MutableLiveData<ToDo> = MutableLiveData(todo)
24+
val isComplete: MutableLiveData<Boolean> = MutableLiveData(todo.isComplete)
25+
val isSaving: MutableLiveData<Boolean> = MutableLiveData(false)
26+
27+
private var currentIsCompleteState: Boolean = isComplete.requireValue()
28+
29+
init {
30+
isComplete.observeNonNull(lifecycleOwner) {
31+
this.todo.value = this.todo.requireValue().copy(isComplete = it)
32+
}
33+
34+
this.todo.observeNonNull(lifecycleOwner) {
35+
if (it.isComplete == currentIsCompleteState) return@observeNonNull
36+
37+
coroutineScope.launch {
38+
isSaving.value = true
39+
40+
try {
41+
domainService.saveToRepository(it)
42+
currentIsCompleteState = isComplete.requireValue()
43+
} catch (e: Exception) {
44+
if (e is CancellationException) return@launch
45+
46+
Timber.e(e)
47+
navigator.showSnackBar(R.string.to_do_activity_error_update_task_failed)
48+
isComplete.value = currentIsCompleteState
49+
} finally {
50+
isSaving.value = false
51+
}
52+
}
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)