Browse Source

Add viewmodel for otp list, implement RecyclerView.

master
Chris Smith 11 months ago
parent
commit
207ae601ad

+ 17
- 0
app/src/main/java/com/chameth/yaotp/MainActivity.kt View File

@@ -2,12 +2,29 @@ package com.chameth.yaotp
2 2
 
3 3
 import android.os.Bundle
4 4
 import androidx.appcompat.app.AppCompatActivity
5
+import androidx.lifecycle.ViewModelProviders
6
+import androidx.recyclerview.widget.LinearLayoutManager
7
+import com.chameth.yaotp.accounts.parseUri
8
+import com.chameth.yaotp.viewmodel.OtpListViewModel
9
+import kotlinx.android.synthetic.main.activity_main.*
5 10
 
6 11
 class MainActivity : AppCompatActivity() {
7 12
 
13
+    private val viewModel by lazy { ViewModelProviders.of(this).get(OtpListViewModel::class.java) }
14
+
8 15
     override fun onCreate(savedInstanceState: Bundle?) {
9 16
         super.onCreate(savedInstanceState)
10 17
         setContentView(R.layout.activity_main)
18
+
19
+        addDummyData()
20
+
21
+        otpList.layoutManager = LinearLayoutManager(this)
22
+        otpList.adapter = OtpListAdapter(this, viewModel)
23
+    }
24
+
25
+    private fun addDummyData() {
26
+        viewModel.addAccount(parseUri("otpauth://totp/Example%20123:chris@example.com?secret=UIOIPNVJA5YG3T4UYVBXQJ6AFGFWKDVU&issuer=Example%20123&algorithm=SHA1&digits=6&period=30")!!)
27
+        viewModel.addAccount(parseUri("otpauth://hotp/Foobar%20Inc:chris@example.com?secret=UIOIPNVJA5YG3T4UYVBXQJ6AFGFWKDVU&issuer=Foobar%20Inc&algorithm=SHA1&digits=6&period=30&counter=170")!!)
11 28
     }
12 29
 
13 30
 }

+ 30
- 0
app/src/main/java/com/chameth/yaotp/OtpListAdapter.kt View File

@@ -0,0 +1,30 @@
1
+package com.chameth.yaotp
2
+
3
+import android.view.LayoutInflater
4
+import android.view.ViewGroup
5
+import androidx.databinding.DataBindingUtil
6
+import androidx.fragment.app.FragmentActivity
7
+import androidx.recyclerview.widget.RecyclerView
8
+import com.chameth.yaotp.databinding.ItemOtpBinding
9
+import com.chameth.yaotp.viewmodel.OtpListViewModel
10
+
11
+class OtpListAdapter(private val owner: FragmentActivity, private val viewModel: OtpListViewModel) : RecyclerView.Adapter<OtpListAdapter.OtpViewHolder>() {
12
+
13
+    class OtpViewHolder(val binding: ItemOtpBinding) : RecyclerView.ViewHolder(binding.root)
14
+
15
+    init {
16
+        viewModel.addObserver(object : OtpListViewModel.AccountObserver {
17
+            override fun accountAdded(position: Int) = notifyItemInserted(position)
18
+            override fun accountRemoved(position: Int) = notifyItemRemoved(position)
19
+        })
20
+    }
21
+
22
+    override fun getItemCount() = viewModel.numberOfAccounts
23
+    override fun onCreateViewHolder(parent: ViewGroup, itemType: Int) = OtpViewHolder(inflateWithBinding(parent)).also { it.binding.setLifecycleOwner(owner) }
24
+    override fun onBindViewHolder(viewHolder: OtpViewHolder, position: Int) {
25
+        viewHolder.binding.viewmodel = viewModel.getViewModelForAccount(position)
26
+    }
27
+
28
+    private fun inflateWithBinding(parent: ViewGroup) = DataBindingUtil.inflate<ItemOtpBinding>(LayoutInflater.from(parent.context), R.layout.item_otp, parent, false)
29
+
30
+}

+ 41
- 0
app/src/main/java/com/chameth/yaotp/viewmodel/OtpListViewModel.kt View File

@@ -0,0 +1,41 @@
1
+package com.chameth.yaotp.viewmodel
2
+
3
+import androidx.lifecycle.ViewModel
4
+import com.chameth.yaotp.accounts.Account
5
+import java.util.concurrent.CopyOnWriteArrayList
6
+
7
+class OtpListViewModel : ViewModel() {
8
+
9
+    private val observers = CopyOnWriteArrayList<AccountObserver>()
10
+    private val accounts = ArrayList<Account>()
11
+    private val accountViewModels = ArrayList<OtpItemViewModel>()
12
+
13
+    val numberOfAccounts: Int
14
+        get() = accounts.size
15
+
16
+    fun getViewModelForAccount(accountId: Int) = accountViewModels[accountId]
17
+
18
+    fun addAccount(account: Account) {
19
+        accounts.add(account)
20
+        accountViewModels.add(OtpItemViewModel().also { it.account = account })
21
+        observers.forEach { it.accountAdded(accounts.size - 1) }
22
+    }
23
+
24
+    fun removeAccount(account: Account) {
25
+        val index = accounts.indexOf(account)
26
+        if (index >= 0) {
27
+            accounts.removeAt(index)
28
+            accountViewModels.removeAt(index)
29
+            observers.forEach { it.accountRemoved(index) }
30
+        }
31
+    }
32
+
33
+    fun addObserver(observer: AccountObserver) = observers.add(observer)
34
+    fun removeObserver(observer: AccountObserver) = observers.remove(observer)
35
+
36
+    interface AccountObserver {
37
+        fun accountAdded(position: Int)
38
+        fun accountRemoved(position: Int)
39
+    }
40
+
41
+}

+ 6
- 11
app/src/main/res/layout/activity_main.xml View File

@@ -1,19 +1,14 @@
1 1
 <?xml version="1.0" encoding="utf-8"?>
2
-<android.support.constraint.ConstraintLayout
2
+<LinearLayout
3 3
         xmlns:android="http://schemas.android.com/apk/res/android"
4 4
         xmlns:tools="http://schemas.android.com/tools"
5
-        xmlns:app="http://schemas.android.com/apk/res-auto"
6 5
         android:layout_width="match_parent"
7 6
         android:layout_height="match_parent"
8 7
         tools:context=".MainActivity">
9 8
 
10
-    <TextView
11
-            android:layout_width="wrap_content"
12
-            android:layout_height="wrap_content"
13
-            android:text="Hello World!"
14
-            app:layout_constraintBottom_toBottomOf="parent"
15
-            app:layout_constraintLeft_toLeftOf="parent"
16
-            app:layout_constraintRight_toRightOf="parent"
17
-            app:layout_constraintTop_toTopOf="parent"/>
9
+    <androidx.recyclerview.widget.RecyclerView
10
+            android:id="@+id/otpList"
11
+            android:layout_width="match_parent"
12
+            android:layout_height="match_parent"/>
18 13
 
19
-</android.support.constraint.ConstraintLayout>
14
+</LinearLayout>

+ 90
- 0
app/src/test/java/com/chameth/yaotp/viewmodel/OtpListViewModelTest.kt View File

@@ -0,0 +1,90 @@
1
+package com.chameth.yaotp.viewmodel
2
+
3
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4
+import com.chameth.yaotp.accounts.Account
5
+import com.nhaarman.mockitokotlin2.*
6
+import org.junit.Assert.assertEquals
7
+import org.junit.Rule
8
+import org.junit.Test
9
+
10
+class OtpListViewModelTest {
11
+
12
+    @get:Rule
13
+    val instantTaskExecutorRule = InstantTaskExecutorRule()
14
+
15
+    private val viewModel = OtpListViewModel()
16
+    private val observer = mock<OtpListViewModel.AccountObserver>()
17
+
18
+    @Test
19
+    fun testAddAccount_incrementsNumberOfAccounts() {
20
+        assertEquals(0, viewModel.numberOfAccounts)
21
+        viewModel.addAccount(mock())
22
+        assertEquals(1, viewModel.numberOfAccounts)
23
+        viewModel.addAccount(mock())
24
+        assertEquals(2, viewModel.numberOfAccounts)
25
+    }
26
+
27
+    @Test
28
+    fun testRemoveAccount_decrementsNumberOfAccounts() {
29
+        val account1 = mock<Account>()
30
+        val account2 = mock<Account>()
31
+        viewModel.addAccount(account1)
32
+        viewModel.addAccount(account2)
33
+        viewModel.removeAccount(account1)
34
+        assertEquals(1, viewModel.numberOfAccounts)
35
+        viewModel.removeAccount(account2)
36
+        assertEquals(0, viewModel.numberOfAccounts)
37
+    }
38
+
39
+    @Test
40
+    fun testRemoveAccount_withNonExistantAccount_doesNothing() {
41
+        val account1 = mock<Account>()
42
+        val account2 = mock<Account>()
43
+        viewModel.addAccount(account1)
44
+        viewModel.addObserver(observer)
45
+        viewModel.removeAccount(account2)
46
+
47
+        verifyNoMoreInteractions(observer)
48
+    }
49
+
50
+    @Test
51
+    fun testAddAccount_notifiesRegisteredObservers() {
52
+        viewModel.addAccount(mock())
53
+        viewModel.addObserver(observer)
54
+        viewModel.addAccount(mock())
55
+        viewModel.removeObserver(observer)
56
+        viewModel.addAccount(mock())
57
+
58
+        verify(observer).accountAdded(1)
59
+        verify(observer, never()).accountAdded(0)
60
+        verify(observer, never()).accountAdded(2)
61
+    }
62
+
63
+    @Test
64
+    fun testRemoveAccount_notifiesRegisteredObservers() {
65
+        val account1 = mock<Account>()
66
+        val account2 = mock<Account>()
67
+        val account3 = mock<Account>()
68
+        viewModel.addAccount(account1)
69
+        viewModel.addAccount(account2)
70
+        viewModel.addAccount(account3)
71
+        viewModel.removeAccount(account1)
72
+        viewModel.addObserver(observer)
73
+        viewModel.removeAccount(account3)
74
+        viewModel.removeObserver(observer)
75
+        viewModel.removeAccount(account2)
76
+
77
+        verify(observer).accountRemoved(1)
78
+        verify(observer, never()).accountRemoved(0)
79
+    }
80
+
81
+    @Test
82
+    fun testGetViewModel_returnsCorrectViewModel() {
83
+        viewModel.addAccount(mock { on { label } doReturn "Foo" })
84
+        viewModel.addAccount(mock { on { label } doReturn "Bar" })
85
+
86
+        assertEquals("Foo", viewModel.getViewModelForAccount(0).label.value)
87
+        assertEquals("Bar", viewModel.getViewModelForAccount(1).label.value)
88
+    }
89
+
90
+}

Loading…
Cancel
Save