Merge pull request #7 from hakodeveloper/structure/implement-navigation

Implement navigation
This commit is contained in:
Carlos Martinez
2020-02-05 21:16:19 -03:00
committed by GitHub
27 changed files with 234 additions and 61 deletions

View File

@@ -8,21 +8,26 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.hako.albumlist.R import com.hako.albumlist.R
import com.hako.albumlist.model.AlbumViewable import com.hako.albumlist.model.AlbumViewable
import com.hako.albumlist.navigation.AlbumlistNavigation
import com.hako.albumlist.viewmodel.AlbumlistViewmodel import com.hako.albumlist.viewmodel.AlbumlistViewmodel
import com.hako.albumlist.widget.AlbumlistAdapter import com.hako.albumlist.widget.AlbumlistAdapter
import com.hako.base.domain.network.RequestStatus import com.hako.base.domain.network.RequestStatus
import com.hako.base.extensions.gone import com.hako.base.extensions.gone
import com.hako.base.extensions.observeNonNull import com.hako.base.extensions.observeNonNull
import com.hako.base.extensions.toast
import com.hako.base.extensions.visible import com.hako.base.extensions.visible
import com.hako.base.navigation.NavigationRouter
import kotlinx.android.synthetic.main.fragment_albumlist.* import kotlinx.android.synthetic.main.fragment_albumlist.*
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber import timber.log.Timber
const val ALBUMLIST_FRAGMENT_BUNDLE_USER_ID = "ALBUMLIST_FRAGMENT_BUNDLE_USER_ID"
class AlbumlistFragment : Fragment() { class AlbumlistFragment : Fragment() {
private val viewModel: AlbumlistViewmodel by viewModel() private val viewModel: AlbumlistViewmodel by viewModel()
private val listAdapter by lazy { AlbumlistAdapter() } private val listAdapter by lazy { AlbumlistAdapter() }
private val navigation: NavigationRouter by inject()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
@@ -32,8 +37,12 @@ class AlbumlistFragment : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setRecycler() setRecycler()
setObservers() setObservers()
// TODO: Get user by bundle doRequest()
viewModel.fetchAlbums(2) }
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() { private fun setObservers() {
@@ -72,7 +81,7 @@ class AlbumlistFragment : Fragment() {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = listAdapter.apply { adapter = listAdapter.apply {
onItemClick = { 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

@@ -9,6 +9,10 @@
android:id="@+id/albumlistFragment" android:id="@+id/albumlistFragment"
android:name="com.hako.albumlist.feature.AlbumlistFragment" android:name="com.hako.albumlist.feature.AlbumlistFragment"
tools:layout="@layout/fragment_albumlist" tools:layout="@layout/fragment_albumlist"
android:label="AlbumlistFragment" /> android:label="{actionTitle}">
<argument
android:name="actionTitle"
app:argType="string" />
</fragment>
</navigation> </navigation>

View File

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

View File

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

View File

@@ -3,20 +3,51 @@ package com.hako.friendlists.view
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.navigation.ui.NavigationUI.setupActionBarWithNavController
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.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() { class MainActivity : AppCompatActivity() {
private val navController by lazy { findNavController(R.id.main_fragment_container) } 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
setupNavigation() setupNavigation()
setupPicasso()
} }
private fun setupNavigation() { private fun setupNavigation() {
navController.setGraph(R.navigation.main_navigation) 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)
} }
setupActionBarWithNavController(this, navController)
}
private fun setupPicasso() {
// Show cache indicator on images just for debug builds
picasso.setIndicatorsEnabled(BuildConfig.DEBUG)
}
override fun onSupportNavigateUp(): Boolean {
navController.navigateUp()
return super.onSupportNavigateUp()
}
} }

View File

@@ -0,0 +1,50 @@
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
// This sets the fragment title, it's referenced in every navigation
const val FRAGMENT_TITLE = "actionTitle"
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)
putString(FRAGMENT_TITLE, event.userName)
})
)
}
}
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

@@ -1,8 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/main_fragment_container" android:id="@+id/main_fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
app:defaultNavHost="true"/> app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -2,11 +2,25 @@
<navigation xmlns:android="http://schemas.android.com/apk/res/android" <navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_navigation" android:id="@+id/main_navigation"
app:startDestination="@id/photolist_navigation"> app:startDestination="@id/userlist_navigation">
<include app:graph="@navigation/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" /> <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" /> <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 package com.hako.base.extensions
import android.os.Bundle
import androidx.annotation.IdRes 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) = fun buildNavigation(@IdRes id: Int, bundle: Bundle = Bundle()) = Pair(id, bundle)
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."
)

View File

@@ -4,6 +4,8 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
fun View.enable() { fun View.enable() {
isEnabled = true isEnabled = true
@@ -37,3 +39,22 @@ fun ViewGroup.inflate(@LayoutRes layout: Int, attachToRoot: Boolean = false): Vi
LayoutInflater LayoutInflater
.from(context) .from(context)
.inflate(layout, this, attachToRoot) .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 org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber import timber.log.Timber
const val PHOTOLIST_FRAGMENT_BUNDLE_ALBUM_ID = "PHOTOLIST_FRAGMENT_BUNDLE_ALBUM_ID"
class PhotolistFragment : Fragment() { class PhotolistFragment : Fragment() {
private val viewModel: PhotolistViewmodel by viewModel() private val viewModel: PhotolistViewmodel by viewModel()
@@ -31,8 +33,12 @@ class PhotolistFragment : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setRecycler() setRecycler()
setObservers() setObservers()
// TODO: Get album by bundle doRequest()
viewModel.fetchPhotos(1) }
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() { private fun setObservers() {

View File

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

View File

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

View File

@@ -9,6 +9,6 @@
android:id="@+id/photolistFragment" android:id="@+id/photolistFragment"
android:name="com.hako.photolist.feature.PhotolistFragment" android:name="com.hako.photolist.feature.PhotolistFragment"
tools:layout="@layout/fragment_photolist" tools:layout="@layout/fragment_photolist"
android:label="PhotolistFragment" /> android:label="Photos" />
</navigation> </navigation>

View File

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

View File

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

View File

@@ -9,6 +9,6 @@
android:id="@+id/userlistFragment" android:id="@+id/userlistFragment"
android:name="com.hako.userlist.feature.UserlistFragment" android:name="com.hako.userlist.feature.UserlistFragment"
tools:layout="@layout/fragment_userlist" tools:layout="@layout/fragment_userlist"
android:label="UserlistFragment" /> android:label="Friendlist" />
</navigation> </navigation>