-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improvement/navigation architecture #73
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package com.architectcoders.equipocinco.common | ||
|
||
import android.os.Bundle | ||
import androidx.fragment.app.Fragment | ||
import com.architectcoders.equipocinco.R | ||
import com.architectcoders.equipocinco.ui.activity.MainActivity | ||
import com.ncapdevi.fragnav.FragNavController | ||
import com.ncapdevi.fragnav.FragNavLogger | ||
import com.ncapdevi.fragnav.FragNavSwitchController | ||
import com.ncapdevi.fragnav.FragNavTransactionOptions | ||
import com.ncapdevi.fragnav.tabhistory.UniqueTabHistoryStrategy | ||
import kotlinx.android.synthetic.main.activity_main.* | ||
|
||
class FragmentFrameHelper(private val activity: MainActivity) { | ||
companion object { | ||
const val INDEX_POPULAR = FragNavController.TAB1 | ||
const val INDEX_TOP_RATED = FragNavController.TAB2 | ||
const val INDEX_FAVOURITE = FragNavController.TAB3 | ||
} | ||
|
||
private val fragNavController: FragNavController = | ||
FragNavController(activity.supportFragmentManager, R.id.navHostFragment) | ||
|
||
|
||
fun setupNavController(savedInstanceState: Bundle?) { | ||
fragNavController.apply { | ||
rootFragmentListener = activity | ||
createEager = true | ||
fragNavLogger = object : FragNavLogger { | ||
override fun error(message: String, throwable: Throwable) { | ||
} | ||
} | ||
|
||
defaultTransactionOptions = FragNavTransactionOptions.newBuilder().customAnimations( | ||
R.anim.slide_in_from_right, | ||
R.anim.slide_out_to_left, | ||
R.anim.slide_in_from_left, | ||
R.anim.slide_out_to_right | ||
).build() | ||
fragmentHideStrategy = FragNavController.DETACH_ON_NAVIGATE_HIDE_ON_SWITCH | ||
|
||
navigationStrategy = UniqueTabHistoryStrategy(object : FragNavSwitchController { | ||
override fun switchTab(index: Int, transactionOptions: FragNavTransactionOptions?) { | ||
activity.bottom_navigation_view.selectTabAtPosition(index) | ||
} | ||
}) | ||
} | ||
|
||
fragNavController.initialize(INDEX_POPULAR, savedInstanceState) | ||
val initial = savedInstanceState == null | ||
if (initial) { | ||
activity.bottom_navigation_view.selectTabAtPosition(INDEX_POPULAR) | ||
} | ||
|
||
activity.bottom_navigation_view.setOnTabSelectListener({ tabId -> | ||
when (tabId) { | ||
R.id.menu_popular -> fragNavController.switchTab(INDEX_POPULAR) | ||
R.id.menu_top_rated -> fragNavController.switchTab(INDEX_TOP_RATED) | ||
R.id.menu_favourites -> fragNavController.switchTab(INDEX_FAVOURITE) | ||
} | ||
}, initial) | ||
|
||
activity.bottom_navigation_view.setOnTabReselectListener { fragNavController.clearStack() } | ||
|
||
|
||
} | ||
|
||
fun pushFragment(fragment: Fragment) { | ||
fragNavController.pushFragment(fragment) | ||
} | ||
|
||
fun onSaveInstanceState(outState: Bundle) { | ||
fragNavController.onSaveInstanceState(outState) | ||
} | ||
|
||
fun popFragmentNot(): Boolean = fragNavController.popFragment().not() | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.architectcoders.equipocinco.extensions | ||
|
||
import android.os.Bundle | ||
import com.architectcoders.equipocinco.ui.fragment.detail.DetailMovieFragment | ||
import com.architectcoders.equipocinco.ui.fragment.master.child.FavouriteMoviesFragment | ||
import com.architectcoders.equipocinco.ui.fragment.master.child.PopularMoviesFragment | ||
import com.architectcoders.equipocinco.ui.fragment.master.child.TopRatedMoviesFragment | ||
|
||
fun PopularMoviesFragment.Companion.newInstance(): PopularMoviesFragment = PopularMoviesFragment() | ||
|
||
fun TopRatedMoviesFragment.Companion.newInstance(): TopRatedMoviesFragment = TopRatedMoviesFragment() | ||
|
||
fun FavouriteMoviesFragment.Companion.newInstance(): FavouriteMoviesFragment = FavouriteMoviesFragment() | ||
|
||
//Detail Movie instance | ||
fun DetailMovieFragment.Companion.newInstance(id: Int): DetailMovieFragment { | ||
val args = Bundle() | ||
args.putInt(MOVIE_ID_KEY, id) | ||
val fragment = DetailMovieFragment() | ||
fragment.arguments = args | ||
return fragment | ||
|
||
} | ||
|
||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,59 @@ | ||
package com.architectcoders.equipocinco.ui.activity | ||
|
||
import android.os.Bundle | ||
import android.view.View | ||
import androidx.appcompat.app.AppCompatActivity | ||
import androidx.navigation.Navigation | ||
import androidx.navigation.findNavController | ||
import androidx.navigation.fragment.NavHostFragment | ||
import androidx.navigation.ui.NavigationUI | ||
import androidx.navigation.ui.setupWithNavController | ||
import androidx.fragment.app.Fragment | ||
import com.architectcoders.equipocinco.R | ||
import kotlinx.android.synthetic.main.activity_main.* | ||
|
||
class MainActivity : AppCompatActivity() { | ||
|
||
import com.architectcoders.equipocinco.common.FragmentFrameHelper | ||
import com.architectcoders.equipocinco.common.FragmentFrameHelper.Companion.INDEX_FAVOURITE | ||
import com.architectcoders.equipocinco.common.FragmentFrameHelper.Companion.INDEX_POPULAR | ||
import com.architectcoders.equipocinco.common.FragmentFrameHelper.Companion.INDEX_TOP_RATED | ||
import com.architectcoders.equipocinco.extensions.newInstance | ||
import com.architectcoders.equipocinco.ui.fragment.BaseFragment | ||
import com.architectcoders.equipocinco.ui.fragment.master.child.FavouriteMoviesFragment | ||
import com.architectcoders.equipocinco.ui.fragment.master.child.PopularMoviesFragment | ||
import com.architectcoders.equipocinco.ui.fragment.master.child.TopRatedMoviesFragment | ||
import com.ncapdevi.fragnav.FragNavController | ||
|
||
class MainActivity : AppCompatActivity(), FragNavController.RootFragmentListener, | ||
BaseFragment.FragmentNavigation { | ||
|
||
private val fragmentHelper = FragmentFrameHelper(this) | ||
private lateinit var activity: MainActivity | ||
override val numberOfRootFragments: Int = 3 | ||
|
||
|
||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
setTheme(R.style.AppTheme) | ||
setContentView(R.layout.activity_main) | ||
fragmentHelper.setupNavController(savedInstanceState) | ||
activity = this@MainActivity | ||
setupBottomNavigation() | ||
} | ||
|
||
private fun setupBottomNavigation() { | ||
bottom_navigation_view.setupWithNavController(findNavController(R.id.navHostFragment)) | ||
|
||
override fun onSaveInstanceState(outState: Bundle) { | ||
super.onSaveInstanceState(outState) | ||
fragmentHelper.onSaveInstanceState(outState) | ||
} | ||
|
||
override fun onSupportNavigateUp() = | ||
Navigation.findNavController(this, R.id.navHostFragment).navigateUp() | ||
} | ||
override fun onBackPressed() { | ||
if (fragmentHelper.popFragmentNot()) { | ||
super.onBackPressed() | ||
} | ||
} | ||
|
||
override fun getRootFragment(index: Int): Fragment { | ||
when (index) { | ||
INDEX_POPULAR -> { return PopularMoviesFragment.newInstance() } | ||
INDEX_TOP_RATED -> return TopRatedMoviesFragment.newInstance() | ||
INDEX_FAVOURITE -> return FavouriteMoviesFragment.newInstance() | ||
} | ||
throw IllegalStateException("Need to send an index that we know") | ||
} | ||
|
||
override fun pushFragment(fragment: Fragment, sharedElementList: List<Pair<View, String>>?) { | ||
fragmentHelper.pushFragment(fragment) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package com.architectcoders.equipocinco.ui.fragment | ||
|
||
import android.content.Context | ||
import android.view.View | ||
import androidx.fragment.app.Fragment | ||
|
||
|
||
abstract class BaseFragment : Fragment() { | ||
|
||
lateinit var mFragmentNavigation: FragmentNavigation | ||
|
||
override fun onAttach(context: Context) { | ||
super.onAttach(context) | ||
if (context is FragmentNavigation) { | ||
mFragmentNavigation = context | ||
} | ||
} | ||
|
||
interface FragmentNavigation { | ||
fun pushFragment(fragment: Fragment, sharedElementList: List<Pair<View, String>>? = null) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,11 +5,7 @@ import android.os.Bundle | |
import android.view.LayoutInflater | ||
import android.view.View | ||
import android.view.ViewGroup | ||
import androidx.core.os.bundleOf | ||
import androidx.fragment.app.Fragment | ||
import androidx.lifecycle.Observer | ||
import androidx.navigation.NavController | ||
import androidx.navigation.findNavController | ||
import androidx.recyclerview.widget.GridLayoutManager | ||
import com.architectcoders.domain.model.Movie | ||
import com.architectcoders.equipocinco.R | ||
|
@@ -18,27 +14,27 @@ import com.architectcoders.equipocinco.di.modules.MoviesComponent | |
import com.architectcoders.equipocinco.di.modules.MoviesModule | ||
import com.architectcoders.equipocinco.extensions.app | ||
import com.architectcoders.equipocinco.extensions.getViewModel | ||
import com.architectcoders.equipocinco.extensions.newInstance | ||
import com.architectcoders.equipocinco.framework.SearchManager | ||
import com.architectcoders.equipocinco.ui.adapter.MovieAdapter | ||
import com.architectcoders.equipocinco.ui.fragment.BaseFragment | ||
import com.architectcoders.equipocinco.ui.fragment.detail.DetailMovieFragment | ||
import com.architectcoders.generic.framework.extension.isFilled | ||
import com.architectcoders.generic.framework.extension.view.setVisibleOrGone | ||
import com.architectcoders.presentation.common.Event | ||
import com.architectcoders.presentation.viewmodels.MovieViewModel | ||
import kotlinx.android.synthetic.main.fragment_movies.* | ||
import kotlinx.android.synthetic.main.progress_bar.* | ||
import kotlinx.android.synthetic.main.search.* | ||
|
||
abstract class MoviesFragment : Fragment() { | ||
abstract class MoviesFragment : BaseFragment() { | ||
|
||
private lateinit var navController: NavController | ||
private lateinit var coarsePermissionRequester: PermissionRequester | ||
|
||
private lateinit var component: MoviesComponent | ||
private var adapter: MovieAdapter? = null | ||
|
||
protected val viewModel: MovieViewModel by lazy { getViewModel { component.movieViewModel } } | ||
|
||
private var adapter: MovieAdapter? = null | ||
|
||
override fun onCreateView( | ||
inflater: LayoutInflater, | ||
container: ViewGroup?, | ||
|
@@ -47,7 +43,6 @@ abstract class MoviesFragment : Fragment() { | |
return inflater.inflate(R.layout.fragment_movies, container, false) | ||
} | ||
|
||
|
||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
super.onViewCreated(view, savedInstanceState) | ||
|
||
|
@@ -56,18 +51,22 @@ abstract class MoviesFragment : Fragment() { | |
} ?: throw Exception("Invalid Activity") | ||
|
||
|
||
navController = view.findNavController() | ||
|
||
coarsePermissionRequester = | ||
PermissionRequester(activity, Manifest.permission.ACCESS_COARSE_LOCATION) | ||
|
||
coarsePermissionRequester.request { | ||
viewModel.model.observe(viewLifecycleOwner, Observer(::updateUI)) | ||
} | ||
|
||
navigationObserver() | ||
initClSearch() | ||
} | ||
|
||
|
||
private fun navigationObserver() { | ||
viewModel.modelNavigation.observe(viewLifecycleOwner, Observer(::navigationResult)) | ||
} | ||
|
||
private fun updateUI(model: MovieViewModel.UiModel) { | ||
when (model) { | ||
is MovieViewModel.UiModel.Loading -> pb.show() | ||
|
@@ -76,22 +75,20 @@ abstract class MoviesFragment : Fragment() { | |
} | ||
} | ||
|
||
abstract fun onRequestMovies() | ||
|
||
private fun updateData(movies: List<Movie>) { | ||
initAdapter(movies) | ||
} | ||
|
||
private fun navigationResult(navigationModel: Event<MovieViewModel.NavigationModel>) { | ||
navigationModel.getContentIfNotHandled()?.let { navModel -> | ||
mFragmentNavigation.pushFragment(DetailMovieFragment.newInstance(navModel.movie.id)) | ||
} | ||
} | ||
|
||
private fun initAdapter(items: List<Movie>) { | ||
rv?.let { | ||
rv.layoutManager = GridLayoutManager(activity, 3) | ||
adapter = | ||
MovieAdapter(items.toMutableList()) { | ||
navController.navigate( | ||
R.id.action_moviesFragment_to_detailMovieFragment, | ||
bundleOf(DetailMovieFragment.MOVIE_ID_KEY to it.id) | ||
) | ||
} | ||
adapter = MovieAdapter(items.toMutableList(), viewModel::onSelectedMovie) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. aqui es donde me referia que no rastreabamos la accion del onClick en una peli. Ahora si que le pasamos al viewModel la accion(onSelectedMovie) y de esta manera ya podemos testear este logica-flow There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mola! |
||
rv.adapter = adapter | ||
pb.hide() | ||
} | ||
|
@@ -114,4 +111,7 @@ abstract class MoviesFragment : Fragment() { | |
private fun searchMovies(query: String) { | ||
viewModel.onSearchMovies(query) | ||
} | ||
|
||
abstract fun onRequestMovies() | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aqui puedes personalizar la estrategia del stack de fragments, ahora tenemos DETACH_ON_NAVIGATE_HIDE_ON_SWITCH que basicamente significa q en nuestro caso siempre tendria 3 vistas en memorias a la vez, no importa en que nivel de profundidad estes. Por ejemplo: si estas en detailMovie del TAB de popularMovies, tendrías en memoria estas 3, DetailScreen(de popularMovie), TopRatedScreen y FavouriteScreen , y si le das hacia atras desde DetailScreen, tendrias ahora estas vistas PopularMovie, TopRatedScreen y FavouriteScreen en memoria
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cuando vas en niveles dentro de un mismo TAB, por ej. desde PopularMovies a detail, se hace un detach del fragment de modo que se destruye la View del fragment de PopularMocies pero queda la instancia del objeto en memoria, y en consecuencia el ViewModel sobrevive tambien conteniendo en sus LiveDatas los ultimos valores. Por eso tuve que hacer un wrapper del object Navigation en un Event en el onSelectedMovie y preguntar si ya se habia consumido anteriormente, de otro modo me volveria de nuevo al detailscreen y no me dejaria ir nunca hacia atras
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Brutal! Gracias por la explicación!