add navigation framework and implement it

This commit is contained in:
Carlos Martinez
2020-02-05 15:12:53 -03:00
parent 5877a4851e
commit 8130956484
23 changed files with 200 additions and 52 deletions

View File

@@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import com.hako.albumlist.R
import com.hako.albumlist.model.AlbumViewable
import com.hako.albumlist.navigation.AlbumlistNavigation
import com.hako.albumlist.viewmodel.AlbumlistViewmodel
import com.hako.albumlist.widget.AlbumlistAdapter
import com.hako.base.domain.network.RequestStatus
@@ -15,14 +16,19 @@ import com.hako.base.extensions.gone
import com.hako.base.extensions.observeNonNull
import com.hako.base.extensions.toast
import com.hako.base.extensions.visible
import com.hako.base.navigation.NavigationRouter
import kotlinx.android.synthetic.main.fragment_albumlist.*
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
const val ALBUMLIST_FRAGMENT_BUNDLE_USER_ID = "ALBUMLIST_FRAGMENT_BUNDLE_USER_ID"
class AlbumlistFragment : Fragment() {
private val viewModel: AlbumlistViewmodel by viewModel()
private val listAdapter by lazy { AlbumlistAdapter() }
private val navigation: NavigationRouter by inject()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
@@ -32,8 +38,12 @@ class AlbumlistFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
setRecycler()
setObservers()
// TODO: Get user by bundle
viewModel.fetchAlbums(2)
doRequest()
}
private fun doRequest() {
arguments?.getInt(ALBUMLIST_FRAGMENT_BUNDLE_USER_ID)?.let { viewModel.fetchAlbums(it) } ?:
throw UninitializedPropertyAccessException("The UserId is expected but it wasn't provided")
}
private fun setObservers() {
@@ -72,7 +82,7 @@ class AlbumlistFragment : Fragment() {
layoutManager = LinearLayoutManager(context)
adapter = listAdapter.apply {
onItemClick = {
context.toast(it.title)
navigation.sendNavigation(AlbumlistNavigation.ClickedOnAlbum(it.id))
}
}
}

View File

@@ -0,0 +1,7 @@
package com.hako.albumlist.navigation
import com.hako.base.navigation.NavigationEvent
sealed class AlbumlistNavigation : NavigationEvent {
data class ClickedOnAlbum(val albumId: Int) : AlbumlistNavigation()
}

View File

@@ -15,6 +15,7 @@
<activity
android:name=".view.MainActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@@ -3,8 +3,11 @@ package com.hako.friendlists.di
import androidx.room.Room
import com.hako.base.domain.database.DatabaseClient
import com.hako.base.domain.network.RemoteClient
import com.hako.base.navigation.NavigationRouter
import com.hako.friendlists.BuildConfig
import com.hako.friendlists.viewmodel.NavigationViewmodel
import com.squareup.picasso.Picasso
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val appModules = module {
@@ -19,4 +22,8 @@ val appModules = module {
// Picasso
single { Picasso.get() }
// Navigation
single { NavigationRouter() }
viewModel { NavigationViewmodel() }
}

View File

@@ -3,20 +3,43 @@ package com.hako.friendlists.view
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import com.hako.base.extensions.observeNonNull
import com.hako.base.navigation.NavigationRouter
import com.hako.friendlists.BuildConfig
import com.hako.friendlists.R
import com.hako.friendlists.viewmodel.NavigationViewmodel
import com.squareup.picasso.Picasso
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
class MainActivity : AppCompatActivity() {
private val navController by lazy { findNavController(R.id.main_fragment_container) }
private val navRouter: NavigationRouter by inject()
private val picasso: Picasso by inject()
private val viewModel: NavigationViewmodel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupNavigation()
setupPicasso()
}
private fun setupNavigation() {
navController.setGraph(R.navigation.main_navigation)
navRouter.setOnNavigationEvent {
viewModel.onNavigationEvent(it)
}
viewModel.navigate.observeNonNull(this) { pair ->
// Pair.first is a Navigation Id
// Pair.second is a Bundle
navController.navigate(pair.first, pair.second)
}
}
private fun setupPicasso() {
// Show cache indicator on images just for debug builds
picasso.setIndicatorsEnabled(BuildConfig.DEBUG)
}
}

View File

@@ -0,0 +1,46 @@
package com.hako.friendlists.viewmodel
import android.os.Bundle
import androidx.annotation.IdRes
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.hako.albumlist.feature.ALBUMLIST_FRAGMENT_BUNDLE_USER_ID
import com.hako.albumlist.navigation.AlbumlistNavigation
import com.hako.base.extensions.buildNavigation
import com.hako.base.navigation.NavigationEvent
import com.hako.friendlists.R
import com.hako.photolist.feature.PHOTOLIST_FRAGMENT_BUNDLE_ALBUM_ID
import com.hako.userlist.navigation.UserlistNavigation
class NavigationViewmodel : ViewModel() {
val navigate = MutableLiveData<Pair<@IdRes Int, Bundle>>()
fun onNavigationEvent(event: NavigationEvent) {
when (event) {
is UserlistNavigation -> handleUserlistNavigation(event)
is AlbumlistNavigation -> handleAlbumlistNavigation(event)
else -> throw NoWhenBranchMatchedException("Undefined navigation event parent")
}
}
private fun handleUserlistNavigation(event: UserlistNavigation) {
when (event) {
is UserlistNavigation.ClickedOnUser -> navigate.postValue(
buildNavigation(R.id.action_userlistFragment_to_albumlistFragment, Bundle().apply {
putInt(ALBUMLIST_FRAGMENT_BUNDLE_USER_ID, event.userId)
})
)
}
}
private fun handleAlbumlistNavigation(event: AlbumlistNavigation) {
when (event) {
is AlbumlistNavigation.ClickedOnAlbum -> navigate.postValue(
buildNavigation(R.id.action_albumlistFragment_to_photolistFragment, Bundle().apply {
putInt(PHOTOLIST_FRAGMENT_BUNDLE_ALBUM_ID, event.albumId)
})
)
}
}
}

View File

@@ -2,11 +2,25 @@
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_navigation"
app:startDestination="@id/photolist_navigation">
app:startDestination="@id/userlist_navigation">
<include app:graph="@navigation/userlist_navigation" />
<action
android:id="@+id/action_userlistFragment_to_albumlistFragment"
app:destination="@id/albumlist_navigation"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"/>
<include app:graph="@navigation/albumlist_navigation" />
<action
android:id="@+id/action_albumlistFragment_to_photolistFragment"
app:destination="@id/photolist_navigation"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"/>
<include app:graph="@navigation/photolist_navigation" />

View File

@@ -1,23 +0,0 @@
package com.hako.base.extensions
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
fun <T> RecyclerView.Adapter<*>.autoNotify(oldList: List<T>, newList: List<T>, compare: (T, T) -> Boolean) {
val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return compare(oldList[oldItemPosition], newList[newItemPosition])
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition]
}
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
})
diff.dispatchUpdatesTo(this)
}

View File

@@ -1,19 +1,6 @@
package com.hako.base.extensions
import android.os.Bundle
import androidx.annotation.IdRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.NavHostFragment
fun AppCompatActivity.findNavHostFragment(@IdRes id: Int) =
supportFragmentManager.findFragmentById(id) as NavHostFragment
fun Fragment.findNavHostFragment(@IdRes id: Int) =
childFragmentManager.findFragmentById(id) as NavHostFragment
fun Fragment.findNavController(@IdRes id: Int) =
androidx.navigation.Navigation.findNavController(view?.findViewById(id) ?: viewNotFound(id, this))
private fun viewNotFound(@IdRes id: Int, fragment: Fragment): Nothing = throw IllegalStateException(
"View ID $id at '${fragment::class.java.simpleName}' not found."
)
fun buildNavigation(@IdRes id: Int, bundle: Bundle = Bundle()) = Pair(id, bundle)

View File

@@ -4,6 +4,8 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
fun View.enable() {
isEnabled = true
@@ -37,3 +39,22 @@ fun ViewGroup.inflate(@LayoutRes layout: Int, attachToRoot: Boolean = false): Vi
LayoutInflater
.from(context)
.inflate(layout, this, attachToRoot)
fun <T> RecyclerView.Adapter<*>.autoNotify(oldList: List<T>, newList: List<T>, compare: (T, T) -> Boolean) {
val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return compare(oldList[oldItemPosition], newList[newItemPosition])
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition]
}
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
})
diff.dispatchUpdatesTo(this)
}

View File

@@ -0,0 +1,7 @@
package com.hako.base.navigation
interface NavigationEvent
interface NavigationController {
fun sendNavigation(event: NavigationEvent)
}

View File

@@ -0,0 +1,13 @@
package com.hako.base.navigation
class NavigationRouter : NavigationController {
private var onNavigationEvent: (NavigationEvent) -> Unit = {}
override fun sendNavigation(event: NavigationEvent) {
onNavigationEvent(event)
}
fun setOnNavigationEvent(listener: (NavigationEvent) -> Unit) {
onNavigationEvent = listener
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="-100%p" android:toXDelta="0"
android:duration="@android:integer/config_shortAnimTime"/>
</set>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="100%p" android:toXDelta="0"
android:duration="@android:integer/config_shortAnimTime"/>
</set>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="-100%p"
android:duration="@android:integer/config_shortAnimTime"/>
</set>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="100%p"
android:duration="@android:integer/config_shortAnimTime"/>
</set>

View File

@@ -18,6 +18,8 @@ import kotlinx.android.synthetic.main.fragment_photolist.*
import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
const val PHOTOLIST_FRAGMENT_BUNDLE_ALBUM_ID = "PHOTOLIST_FRAGMENT_BUNDLE_ALBUM_ID"
class PhotolistFragment : Fragment() {
private val viewModel: PhotolistViewmodel by viewModel()
@@ -31,8 +33,12 @@ class PhotolistFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
setRecycler()
setObservers()
// TODO: Get album by bundle
viewModel.fetchPhotos(1)
doRequest()
}
private fun doRequest() {
arguments?.getInt(PHOTOLIST_FRAGMENT_BUNDLE_ALBUM_ID)?.let { viewModel.fetchPhotos(it) } ?:
throw UninitializedPropertyAccessException("The AlbumId is expected but it wasn't provided")
}
private fun setObservers() {

View File

@@ -47,10 +47,6 @@ class PhotoViewHolder(private val view: View) :
private val picasso: Picasso by inject()
init {
picasso.setIndicatorsEnabled(true)
}
fun bind(photo: PhotoViewable) = with(view) {
picasso.load(photo.photoUrl)
.placeholder(R.drawable.img_photo_placeholder)

View File

@@ -35,9 +35,11 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:lines="2"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"

View File

@@ -11,11 +11,14 @@ import com.hako.base.extensions.gone
import com.hako.base.extensions.observeNonNull
import com.hako.base.extensions.toast
import com.hako.base.extensions.visible
import com.hako.base.navigation.NavigationRouter
import com.hako.userlist.model.UserViewable
import com.hako.userlist.viewmodel.UserlistViewmodel
import com.hako.userlist.widget.UserlistAdapter
import com.hako.friendlist_userlist.R
import com.hako.userlist.navigation.UserlistNavigation
import kotlinx.android.synthetic.main.fragment_userlist.*
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
@@ -23,6 +26,7 @@ class UserlistFragment : Fragment() {
private val viewModel: UserlistViewmodel by viewModel()
private val listAdapter by lazy { UserlistAdapter() }
private val navigation: NavigationRouter by inject()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
@@ -71,7 +75,7 @@ class UserlistFragment : Fragment() {
layoutManager = LinearLayoutManager(context)
adapter = listAdapter.apply {
onItemClick = {
context.toast(it.realName)
navigation.sendNavigation(UserlistNavigation.ClickedOnUser(it.id))
}
onFavoriteClick = {

View File

@@ -0,0 +1,7 @@
package com.hako.userlist.navigation
import com.hako.base.navigation.NavigationEvent
sealed class UserlistNavigation : NavigationEvent {
data class ClickedOnUser(val userId: Int) : UserlistNavigation()
}

View File

@@ -17,7 +17,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginTop="12dp"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"