diff --git a/app/build.gradle b/app/build.gradle
index b6a859d..b3ee4c9 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -37,6 +37,8 @@ android {
branchNames = ["master"] // branch names from which you can deploy, master by default
remoteRepoName = "origin" // alias repository, origin by default
}
+
+ multiDexEnabled true
}
signingConfigs {
debug {
@@ -105,12 +107,19 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation 'com.google.code.gson:gson:2.8.6'
+ implementation "androidx.paging:paging-runtime-ktx:2.1.2"
+ implementation "com.google.android.material:material:1.2.1"
+ implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
+ implementation 'com.android.support:multidex:1.0.3'
+ implementation "com.squareup.okhttp3:logging-interceptor:3.12.8"
implementation androidLibs
implementation apolloLibs
implementation coroutinesLibs
implementation kodeinLibs
implementation roomLibs
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
kapt compilerLibs
testImplementation unitTestLibs
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index aafc5de..bc92766 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -14,6 +14,9 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
+
@@ -31,4 +34,4 @@
-
\ No newline at end of file
+
diff --git a/app/src/main/graphql/com/flatstack/android/graphql/query/GetActivities.graphql b/app/src/main/graphql/com/flatstack/android/graphql/query/GetActivities.graphql
new file mode 100644
index 0000000..f76c6e5
--- /dev/null
+++ b/app/src/main/graphql/com/flatstack/android/graphql/query/GetActivities.graphql
@@ -0,0 +1,40 @@
+query GetActivities($after: String, $before: String, $events: [ActivityEvent!], $first: Int, $last: Int) {
+ activities(
+ after: $after,
+ before: $before,
+ events: $events,
+ first: $first,
+ last: $last
+ ) {
+ ...ActivityConnectionFragment
+ }
+}
+
+fragment ActivityConnectionFragment on ActivityConnection {
+ edges {
+ node {
+ ...ActivityFragment
+ }
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ }
+}
+
+
+fragment ActivityFragment on Activity {
+ body,
+ createdAt,
+ event,
+ id,
+ title,
+ user {
+ firstName,
+ lastName,
+ email,
+ avatarUrl
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/flatstack/android/Router.kt b/app/src/main/java/com/flatstack/android/Router.kt
index ae126e4..fedf287 100644
--- a/app/src/main/java/com/flatstack/android/Router.kt
+++ b/app/src/main/java/com/flatstack/android/Router.kt
@@ -2,6 +2,7 @@ package com.flatstack.android
import android.content.Context
import android.content.Intent
+import com.flatstack.android.activities.UserActivitiesActivity
import com.flatstack.android.login.LoginActivity
import com.flatstack.android.profile.ProfileActivity
@@ -23,4 +24,12 @@ class Router(
}
})
}
+
+ fun activities(context: Context, clearStack: Boolean = false) {
+ context.startActivity(Intent(context, UserActivitiesActivity::class.java).apply {
+ if (clearStack) {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
+ })
+ }
}
diff --git a/app/src/main/java/com/flatstack/android/activities/ActivitiesAdapter.kt b/app/src/main/java/com/flatstack/android/activities/ActivitiesAdapter.kt
new file mode 100644
index 0000000..7fdfeeb
--- /dev/null
+++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesAdapter.kt
@@ -0,0 +1,18 @@
+package com.flatstack.android.activities
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.paging.PagedListAdapter
+import com.flatstack.android.R
+
+class ActivitiesAdapter :
+ PagedListAdapter(ActivitiesDiffUtilCallback()) {
+
+ override fun onBindViewHolder(holder: ActivitiesViewHolder, position: Int) {
+ getItem(position)?.let { holder.bind(it) }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ActivitiesViewHolder(
+ LayoutInflater.from(parent.context).inflate(R.layout.item_activity, parent, false)
+ )
+}
diff --git a/app/src/main/java/com/flatstack/android/activities/ActivitiesDiffUtilCallback.kt b/app/src/main/java/com/flatstack/android/activities/ActivitiesDiffUtilCallback.kt
new file mode 100644
index 0000000..27f22b8
--- /dev/null
+++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesDiffUtilCallback.kt
@@ -0,0 +1,15 @@
+package com.flatstack.android.activities
+
+import androidx.recyclerview.widget.DiffUtil
+
+class ActivitiesDiffUtilCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: ActivitiesViewHolderModel,
+ newItem: ActivitiesViewHolderModel
+ ): Boolean = oldItem.id == newItem.id
+
+ override fun areContentsTheSame(
+ oldItem: ActivitiesViewHolderModel,
+ newItem: ActivitiesViewHolderModel
+ ): Boolean = oldItem == newItem
+}
diff --git a/app/src/main/java/com/flatstack/android/activities/ActivitiesRepository.kt b/app/src/main/java/com/flatstack/android/activities/ActivitiesRepository.kt
new file mode 100644
index 0000000..e0e21cc
--- /dev/null
+++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesRepository.kt
@@ -0,0 +1,125 @@
+package com.flatstack.android.activities
+
+import androidx.lifecycle.asFlow
+import androidx.paging.DataSource
+import androidx.paging.LivePagedListBuilder
+import androidx.paging.PageKeyedDataSource
+import androidx.paging.PagedList
+import com.apollographql.apollo.ApolloClient
+import com.apollographql.apollo.api.Input
+import com.apollographql.apollo.coroutines.toDeferred
+import com.flatstack.android.fragment.ActivityConnectionFragment.Edge
+import com.flatstack.android.fragment.ActivityConnectionFragment.PageInfo
+import com.flatstack.android.graphql.query.GetActivitiesQuery
+import com.flatstack.android.profile.entities.Profile
+import com.flatstack.android.type.ActivityEvent
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class ActivitiesRepository(
+ private val apolloClient: ApolloClient
+) {
+ @Suppress("LongMethod")
+ fun getPagedUserActivities(coroutineScope: CoroutineScope, events: List) =
+ LivePagedListBuilder(
+ object : DataSource.Factory() {
+ override fun create(): DataSource =
+ object : PageKeyedDataSource() {
+ override fun loadInitial(
+ params: LoadInitialParams,
+ callback: LoadInitialCallback
+ ) {
+ coroutineScope.launch(Dispatchers.IO) {
+ apolloClient.query(
+ GetActivitiesQuery(
+ events = Input.optional(events),
+ first = Input.optional(PAGE_SIZE)
+ )
+ ).toDeferred()
+ .await().data?.activities?.fragments?.activityConnectionFragment?.let {
+ it.edges?.let { edges ->
+ callback.onResult(
+ edges,
+ null,
+ it.pageInfo
+ )
+ }
+ }
+ }
+ }
+
+ override fun loadAfter(
+ params: LoadParams,
+ callback: LoadCallback
+ ) {
+ coroutineScope.launch(Dispatchers.IO) {
+ apolloClient.query(
+ GetActivitiesQuery(
+ after = Input.optional(params.key.endCursor),
+ events = Input.optional(events),
+ first = Input.optional(PAGE_SIZE)
+ )
+ ).toDeferred()
+ .await().data?.activities?.fragments?.activityConnectionFragment?.let {
+ it.edges?.let { edges ->
+ callback.onResult(
+ edges,
+ it.pageInfo
+ )
+ }
+ }
+ }
+ }
+
+ override fun loadBefore(
+ params: LoadParams,
+ callback: LoadCallback
+ ) {
+ coroutineScope.launch(Dispatchers.IO) {
+ apolloClient.query(
+ GetActivitiesQuery(
+ before = Input.optional(params.key.startCursor),
+ events = Input.optional(events),
+ first = Input.optional(PAGE_SIZE)
+ )
+ ).toDeferred()
+ .await().data?.activities?.fragments?.activityConnectionFragment?.let {
+ it.edges?.let { edges ->
+ callback.onResult(
+ edges,
+ it.pageInfo
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ .mapByPage { edges ->
+ edges.map { edge ->
+ edge.node?.fragments?.activityFragment?.let { fragment ->
+ ActivitiesViewHolderModel(
+ body = fragment.body,
+ createdAt = fragment.createdAt.toString(),
+ event = fragment.event,
+ id = fragment.id,
+ title = fragment.title,
+ user = Profile(
+ firstName = fragment.user.firstName ?: "",
+ lastName = fragment.user.lastName ?: ""
+ )
+ )
+ }
+ }
+ },
+ PagedList.Config.Builder()
+ .setEnablePlaceholders(false)
+ .setPageSize(PAGE_SIZE)
+ .build()
+ ).build().asFlow()
+
+ companion object {
+ private const val PAGE_SIZE = 15
+ }
+}
diff --git a/app/src/main/java/com/flatstack/android/activities/ActivitiesViewHolder.kt b/app/src/main/java/com/flatstack/android/activities/ActivitiesViewHolder.kt
new file mode 100644
index 0000000..635a9d2
--- /dev/null
+++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesViewHolder.kt
@@ -0,0 +1,27 @@
+package com.flatstack.android.activities
+
+import android.view.View
+import com.flatstack.android.R
+import com.flatstack.android.util.recyclerview.BaseHolder
+import kotlinx.android.synthetic.main.item_activity.view.*
+import java.text.SimpleDateFormat
+import java.util.*
+
+class ActivitiesViewHolder(itemView: View) : BaseHolder(itemView) {
+
+ override fun bind(item: ActivitiesViewHolderModel) {
+ with(containerView) {
+ tv_event.text = item.title
+ tv_body.text = item.body
+ tv_created_at.text = SimpleDateFormat("HH:mm, dd MMM yyyy", Locale.ENGLISH).format(
+ SimpleDateFormat("yyyy-mm-dd'T'HH:MM:ss'Z'", Locale.ENGLISH).parse(item.createdAt)
+ )
+ tv_username.text = itemView.context.getString(
+ R.string.username_mask,
+ item.user.firstName,
+ item.user.lastName
+ )
+ tv_id.text = itemView.context.getString(R.string.id_mask, item.id)
+ }
+ }
+}
diff --git a/app/src/main/java/com/flatstack/android/activities/ActivitiesViewHolderModel.kt b/app/src/main/java/com/flatstack/android/activities/ActivitiesViewHolderModel.kt
new file mode 100644
index 0000000..3bba719
--- /dev/null
+++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesViewHolderModel.kt
@@ -0,0 +1,13 @@
+package com.flatstack.android.activities
+
+import com.flatstack.android.profile.entities.Profile
+import com.flatstack.android.type.ActivityEvent
+
+data class ActivitiesViewHolderModel(
+ val id: String,
+ val body: String,
+ val createdAt: String,
+ val event: ActivityEvent,
+ val title: String,
+ val user: Profile
+)
diff --git a/app/src/main/java/com/flatstack/android/activities/ActivitiesViewModel.kt b/app/src/main/java/com/flatstack/android/activities/ActivitiesViewModel.kt
new file mode 100644
index 0000000..96ed517
--- /dev/null
+++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesViewModel.kt
@@ -0,0 +1,45 @@
+package com.flatstack.android.activities
+
+import androidx.lifecycle.*
+import androidx.paging.PagedList
+import com.flatstack.android.model.entities.Resource
+import com.flatstack.android.type.ActivityEvent
+import com.flatstack.android.type.ActivityEvent.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+
+class ActivitiesViewModel(
+ private val activitiesRepository: ActivitiesRepository
+) : ViewModel() {
+
+ private val events = MutableLiveData>()
+
+ init {
+ events.postValue(
+ listOf(
+ RESET_PASSWORD_REQUESTED,
+ USER_LOGGED_IN,
+ USER_REGISTERED,
+ USER_RESET_PASSWORD,
+ USER_UPDATED
+ )
+ )
+ }
+
+ val activities: LiveData>> =
+ events.switchMap { events ->
+ activitiesRepository.getPagedUserActivities(viewModelScope, events)
+ .flowOn(Dispatchers.IO)
+ .map { Resource.success(it) }
+ .onStart { emit(Resource.loading()) }
+ .catch { error ->
+ error.message?.let {
+ emit(Resource.error(it))
+ }
+ }
+ .asLiveData(viewModelScope.coroutineContext)
+ }
+}
diff --git a/app/src/main/java/com/flatstack/android/activities/UserActivitiesActivity.kt b/app/src/main/java/com/flatstack/android/activities/UserActivitiesActivity.kt
new file mode 100644
index 0000000..305fccd
--- /dev/null
+++ b/app/src/main/java/com/flatstack/android/activities/UserActivitiesActivity.kt
@@ -0,0 +1,51 @@
+package com.flatstack.android.activities
+
+import android.os.Bundle
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.isVisible
+import androidx.paging.PagedList
+import com.flatstack.android.R
+import com.flatstack.android.util.observeBy
+import com.flatstack.android.util.provideViewModel
+import kotlinx.android.synthetic.main.activity_user_activities.*
+import org.kodein.di.Kodein
+import org.kodein.di.KodeinAware
+import org.kodein.di.android.kodein
+
+class UserActivitiesActivity : AppCompatActivity(), KodeinAware {
+
+ override val kodein: Kodein by kodein()
+
+ private val viewModel: ActivitiesViewModel by provideViewModel()
+ private val adapter = ActivitiesAdapter()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_user_activities)
+ initRecycler()
+
+ viewModel.activities.observeBy(
+ this,
+ onNext = ::fetchActivities,
+ onLoading = ::showProgress,
+ onError = ::showError
+ )
+ }
+
+ private fun fetchActivities(pagedList: PagedList) {
+ adapter.submitList(pagedList)
+ }
+
+ private fun showProgress(isLoading: Boolean) {
+ pb_progress.isVisible = isLoading
+ }
+
+ private fun showError(errorMessage: String) {
+ Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show()
+ }
+
+ private fun initRecycler() {
+ rv_activities.adapter = adapter
+ }
+}
diff --git a/app/src/main/java/com/flatstack/android/di/modules/netModule.kt b/app/src/main/java/com/flatstack/android/di/modules/netModule.kt
index d13c1d1..563b68c 100644
--- a/app/src/main/java/com/flatstack/android/di/modules/netModule.kt
+++ b/app/src/main/java/com/flatstack/android/di/modules/netModule.kt
@@ -4,6 +4,7 @@ import com.apollographql.apollo.ApolloClient
import com.flatstack.android.BuildConfig
import okhttp3.Interceptor
import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
import org.kodein.di.Kodein
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
@@ -13,7 +14,8 @@ val netModule = Kodein.Module(name = "netModule") {
bind() with singleton { AuthorizationInterceptor(instance()) }
bind() with singleton { OkHttpClient.Builder()
- .addInterceptor(instance())
+ .addInterceptor(AuthorizationInterceptor(instance()))
+ .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.build() }
bind() with singleton {
diff --git a/app/src/main/java/com/flatstack/android/di/modules/repoModule.kt b/app/src/main/java/com/flatstack/android/di/modules/repoModule.kt
index d3f702f..1171606 100644
--- a/app/src/main/java/com/flatstack/android/di/modules/repoModule.kt
+++ b/app/src/main/java/com/flatstack/android/di/modules/repoModule.kt
@@ -1,5 +1,6 @@
package com.flatstack.android.di.modules
+import com.flatstack.android.activities.ActivitiesRepository
import com.flatstack.android.login.LoginRepository
import com.flatstack.android.profile.ProfileRepository
import org.kodein.di.Kodein
@@ -10,4 +11,5 @@ import org.kodein.di.generic.provider
val repoModule = Kodein.Module(name = "repoModule") {
bind() with provider { LoginRepository(instance(), instance(), instance(), instance()) }
bind() with provider { ProfileRepository(instance(), instance(), instance()) }
+ bind() with provider { ActivitiesRepository(instance()) }
}
diff --git a/app/src/main/java/com/flatstack/android/di/modules/viewModelModule.kt b/app/src/main/java/com/flatstack/android/di/modules/viewModelModule.kt
index c69b1d3..c567ab5 100644
--- a/app/src/main/java/com/flatstack/android/di/modules/viewModelModule.kt
+++ b/app/src/main/java/com/flatstack/android/di/modules/viewModelModule.kt
@@ -1,6 +1,7 @@
package com.flatstack.android.di.modules
import androidx.lifecycle.ViewModelProvider
+import com.flatstack.android.activities.ActivitiesViewModel
import com.flatstack.android.login.LoginViewModel
import com.flatstack.android.profile.ProfileViewModel
import com.flatstack.android.util.ViewModelFactory
@@ -16,4 +17,5 @@ val viewModelModule = Kodein.Module(name = "viewModelModule") {
bindViewModel() with provider { LoginViewModel(instance(), instance()) }
bindViewModel() with provider { ProfileViewModel(instance(), instance()) }
+ bindViewModel() with provider { ActivitiesViewModel(instance()) }
}
diff --git a/app/src/main/java/com/flatstack/android/profile/ProfileActivity.kt b/app/src/main/java/com/flatstack/android/profile/ProfileActivity.kt
index 8fbdfaa..7c7ef0f 100644
--- a/app/src/main/java/com/flatstack/android/profile/ProfileActivity.kt
+++ b/app/src/main/java/com/flatstack/android/profile/ProfileActivity.kt
@@ -23,7 +23,7 @@ class ProfileActivity : AppCompatActivity(), KodeinAware, OnRefreshListener {
override val kodein: Kodein by kodein()
private val viewModel: ProfileViewModel by provideViewModel()
-
+ private val router by kodein.instance()
private val refreshLayout: SwipeRefreshLayout by lazy { swipe_layout }
override fun onCreate(savedInstanceState: Bundle?) {
@@ -41,6 +41,14 @@ class ProfileActivity : AppCompatActivity(), KodeinAware, OnRefreshListener {
},
onError = ::showError,
onLoading = ::visibleProgress)
+
+ initListeners()
+ }
+
+ private fun initListeners() {
+ bt_go_to_activities.setOnClickListener {
+ router.activities(context = this, clearStack = true)
+ }
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
diff --git a/app/src/main/res/layout/activity_profile.xml b/app/src/main/res/layout/activity_profile.xml
index a447517..304a592 100644
--- a/app/src/main/res/layout/activity_profile.xml
+++ b/app/src/main/res/layout/activity_profile.xml
@@ -40,6 +40,15 @@
app:layout_constraintTop_toBottomOf="@id/tv_first_name"
tools:text="@string/last_name_hint" />
+
+
diff --git a/app/src/main/res/layout/activity_user_activities.xml b/app/src/main/res/layout/activity_user_activities.xml
new file mode 100644
index 0000000..af5a3fc
--- /dev/null
+++ b/app/src/main/res/layout/activity_user_activities.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_activity.xml b/app/src/main/res/layout/item_activity.xml
new file mode 100644
index 0000000..f9f3718
--- /dev/null
+++ b/app/src/main/res/layout/item_activity.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 69b2233..c38c5de 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -3,4 +3,5 @@
#008577
#00574B
#D81B60
+ #F5F5F5
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9ed56aa..d587d2b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -18,4 +18,7 @@
405 Method Not Allowed
409 Conflict
500 Server Error
+ %1$s %2$s
+ Show activities
+ id: %1$s
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index e5448bf..106747d 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -7,4 +7,8 @@
- @color/colorPrimaryDark
- @color/colorAccent
+
+
diff --git a/deps.gradle b/deps.gradle
index 4291992..072985e 100644
--- a/deps.gradle
+++ b/deps.gradle
@@ -8,13 +8,13 @@ ext {
detekt : '1.10.0',
android_junit5 : '1.2.0.0',
kodein : '6.2.0',
- lifecycle : '2.0.0',
+ lifecycle : '2.2.0',
spek : '2.0.4',
junit5_runner : '0.2.2',
mockk : '1.9.3',
mockwebserver : '3.14.0',
retrofit : '2.5.0',
- coroutines : '1.1.1',
+ coroutines : '1.4.2',
room : '2.1.0-beta01',
espressoVersion : '3.1.1'
]
@@ -24,7 +24,10 @@ ext {
constraintlayout: 'androidx.constraintlayout:constraintlayout:1.1.3',
recyclerview : 'androidx.recyclerview:recyclerview:1.0.0',
cardview : 'androidx.cardview:cardview:1.0.0',
- lifecycle : "androidx.lifecycle:lifecycle-extensions:$versions.lifecycle"
+ lifecycle : "androidx.lifecycle:lifecycle-extensions:$versions.lifecycle",
+ livedara : "androidx.lifecycle:lifecycle-livedata-ktx:$versions.lifecycle",
+ common : "androidx.lifecycle:lifecycle-common-java8:$versions.lifecycle",
+ viewmodel : "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.lifecycle"
]
apollo = [
apollo_runtime : "com.apollographql.apollo:apollo-runtime:$versions.apollo",