diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d920e44..0a0c85d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -40,11 +40,14 @@ dependencies { implementation("androidx.constraintlayout:constraintlayout:1.1.3") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.2.0") implementation("androidx.recyclerview:recyclerview:1.1.0") + implementation("androidx.navigation:navigation-runtime-ktx:2.3.0") implementation("androidx.navigation:navigation-fragment-ktx:2.3.0") implementation("androidx.navigation:navigation-ui-ktx:2.3.0") implementation("com.google.android.material:material:1.3.0-alpha01") implementation("com.apollographql.apollo:apollo-runtime:2.2.2") implementation("com.apollographql.apollo:apollo-coroutines-support:2.2.2") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0") + } apollo { diff --git a/app/src/main/java/guide/graphql/toc/ChaptersFragment.kt b/app/src/main/java/guide/graphql/toc/ChaptersFragment.kt deleted file mode 100644 index 70099e5..0000000 --- a/app/src/main/java/guide/graphql/toc/ChaptersFragment.kt +++ /dev/null @@ -1,80 +0,0 @@ -package guide.graphql.toc - -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.apollographql.apollo.coroutines.toDeferred -import com.apollographql.apollo.exception.ApolloException -import com.google.android.material.transition.MaterialSharedAxis -import guide.graphql.toc.databinding.ChaptersFragmentBinding - -class ChaptersFragment : Fragment() { - private lateinit var binding: ChaptersFragmentBinding - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = ChaptersFragmentBinding.inflate(inflater) - return binding.root - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - - val backward = MaterialSharedAxis(MaterialSharedAxis.Z, false) - reenterTransition = backward - - val forward = MaterialSharedAxis(MaterialSharedAxis.Z, true) - exitTransition = forward - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - (requireActivity() as AppCompatActivity).setSupportActionBar(binding.bookHeader) - - lifecycleScope.launchWhenResumed { - val response = try { - apolloClient.query( - ChaptersQuery() - ).toDeferred().await() - } catch (e: ApolloException) { - Log.d("ChaptersQuery", "GraphQL request failed", e) - return@launchWhenResumed - } - - if (response.hasErrors()) { - return@launchWhenResumed - } - - response.data?.chapters?.let { chapters -> - val adapter = - ChaptersAdapter(chapters, requireContext()) { chapter -> - findNavController().navigate( - ChaptersFragmentDirections.viewSections( - chapterId = chapter.id - ) - ) - } - val layoutManager = LinearLayoutManager(requireContext()) - binding.chapters.layoutManager = layoutManager - val itemDivider = DividerItemDecoration(requireContext(), layoutManager.orientation) - binding.chapters.addItemDecoration(itemDivider) - binding.chapters.adapter = adapter - } - - } - } -} \ No newline at end of file diff --git a/app/src/main/java/guide/graphql/toc/MainActivity.kt b/app/src/main/java/guide/graphql/toc/MainActivity.kt index 99b5760..22a4d41 100644 --- a/app/src/main/java/guide/graphql/toc/MainActivity.kt +++ b/app/src/main/java/guide/graphql/toc/MainActivity.kt @@ -2,12 +2,29 @@ package guide.graphql.toc import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.NavController +import androidx.navigation.findNavController +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.NavigationUI +import guide.graphql.toc.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + + setSupportActionBar(binding.toolbar) + NavigationUI.setupActionBarWithNavController(this, findNavController(R.id.nav_host_fragment)) + + } - setContentView(R.layout.activity_main) + override fun onSupportNavigateUp(): Boolean { + return findNavController(R.id.nav_host_fragment).navigateUp() || super.onSupportNavigateUp() } } diff --git a/app/src/main/java/guide/graphql/toc/Resource.kt b/app/src/main/java/guide/graphql/toc/Resource.kt new file mode 100644 index 0000000..1cc68d9 --- /dev/null +++ b/app/src/main/java/guide/graphql/toc/Resource.kt @@ -0,0 +1,23 @@ +package guide.graphql.toc + +//https://github.com/android/architecture-components-samples/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/vo/Resource.kt +data class Resource(val status: Status, val data: T?, val message: String?) { + companion object { + fun success(data: T?): Resource { + return Resource(Status.SUCCESS, data, null) + } + + fun error(msg: String, data: T?): Resource { + return Resource(Status.ERROR, data, msg) + } + + fun loading(data: T?): Resource { + return Resource(Status.LOADING, data, null) + } + } +} +enum class Status { + SUCCESS, + ERROR, + LOADING +} \ No newline at end of file diff --git a/app/src/main/java/guide/graphql/toc/SectionsFragment.kt b/app/src/main/java/guide/graphql/toc/SectionsFragment.kt deleted file mode 100644 index 20e0a69..0000000 --- a/app/src/main/java/guide/graphql/toc/SectionsFragment.kt +++ /dev/null @@ -1,103 +0,0 @@ -package guide.graphql.toc - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import com.apollographql.apollo.coroutines.toDeferred -import com.apollographql.apollo.exception.ApolloException -import com.google.android.material.transition.MaterialSharedAxis -import guide.graphql.toc.databinding.SectionsFragmentBinding - -class SectionsFragment : Fragment() { - - private lateinit var binding: SectionsFragmentBinding - val args: SectionsFragmentArgs by navArgs() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - binding = SectionsFragmentBinding.inflate(inflater) - return binding.root - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val forward = MaterialSharedAxis(MaterialSharedAxis.Z, true) - enterTransition = forward - - val backward = MaterialSharedAxis(MaterialSharedAxis.Z, false) - returnTransition = backward - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - (requireActivity() as AppCompatActivity).setSupportActionBar(binding.chapterHeader) - (requireActivity() as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true) - binding.chapterHeader.setNavigationOnClickListener { - findNavController().navigateUp() - } - - lifecycleScope.launchWhenResumed { - binding.spinner.visibility = View.VISIBLE - binding.error.visibility = View.GONE - - val response = try { - apolloClient.query( - SectionsQuery(id = args.chapterId) - ).toDeferred().await() - } catch (e: ApolloException) { - showErrorMessage(getString(R.string.graphql_error, e.message)) - return@launchWhenResumed - } - - if (response.hasErrors()) { - showErrorMessage(response.errors?.get(0)?.message ?: getString(R.string.error)) - return@launchWhenResumed - } - - response.data?.chapter?.let { chapter -> - val chapterNumber = chapter.number?.toInt() - binding.spinner.visibility = View.GONE - binding.chapterHeader.title = - if (chapter.number == null) chapter.title else getString( - R.string.chapter_title, - chapter.number.toString(), - chapter.title - ) - - if (chapter.sections.size > 1) { - val adapter = - SectionsAdapter(chapterNumber, chapter.sections, requireContext()) - val layoutManager = LinearLayoutManager(requireContext()) - binding.sections.layoutManager = layoutManager - val itemDivider = - DividerItemDecoration(requireContext(), layoutManager.orientation) - binding.sections.addItemDecoration(itemDivider) - binding.sections.adapter = adapter - } else { - binding.error.text = getString(R.string.no_sections) - binding.error.visibility = View.VISIBLE - } - } - - - } - } - - private fun showErrorMessage(error: String) { - binding.spinner.visibility = View.GONE - binding.error.text = error - binding.error.visibility = View.VISIBLE - } -} \ No newline at end of file diff --git a/app/src/main/java/guide/graphql/toc/ChaptersAdapter.kt b/app/src/main/java/guide/graphql/toc/ui/chapters/ChaptersAdapter.kt similarity index 83% rename from app/src/main/java/guide/graphql/toc/ChaptersAdapter.kt rename to app/src/main/java/guide/graphql/toc/ui/chapters/ChaptersAdapter.kt index 0eec207..3139822 100644 --- a/app/src/main/java/guide/graphql/toc/ChaptersAdapter.kt +++ b/app/src/main/java/guide/graphql/toc/ui/chapters/ChaptersAdapter.kt @@ -1,21 +1,28 @@ -package guide.graphql.toc +package guide.graphql.toc.ui.chapters import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import guide.graphql.toc.ChaptersQuery +import guide.graphql.toc.R import guide.graphql.toc.databinding.ChapterBinding class ChaptersAdapter( - private val chapters: List, private val context: Context, + private var chapters: List = listOf(), private val onItemClicked: ((ChaptersQuery.Chapter) -> Unit) ) : RecyclerView.Adapter() { class ViewHolder(val binding: ChapterBinding) : RecyclerView.ViewHolder(binding.root) + fun updateChapters(chapters: List) { + this.chapters = chapters + notifyDataSetChanged() + } + override fun getItemCount(): Int { return chapters.size } diff --git a/app/src/main/java/guide/graphql/toc/ui/chapters/ChaptersFragment.kt b/app/src/main/java/guide/graphql/toc/ui/chapters/ChaptersFragment.kt new file mode 100644 index 0000000..d07dd4c --- /dev/null +++ b/app/src/main/java/guide/graphql/toc/ui/chapters/ChaptersFragment.kt @@ -0,0 +1,88 @@ +package guide.graphql.toc.ui.chapters + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.transition.MaterialFade +import com.google.android.material.transition.MaterialSharedAxis +import guide.graphql.toc.R +import guide.graphql.toc.Status +import guide.graphql.toc.databinding.ChaptersFragmentBinding + +class ChaptersFragment : Fragment() { + + private val viewModel: ChaptersViewModel by viewModels() + + private lateinit var binding: ChaptersFragmentBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = ChaptersFragmentBinding.inflate(inflater) + return binding.root + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val backward = MaterialSharedAxis(MaterialSharedAxis.X, false) + reenterTransition = backward + + val forward = MaterialSharedAxis(MaterialSharedAxis.X, true) + exitTransition = forward + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + requireActivity() + val adapter = + ChaptersAdapter( + requireContext() + ) { chapter -> + findNavController().navigate( + ChaptersFragmentDirections.viewSections( + chapterId = chapter.id, + chapterNumber = chapter.number?.toInt() ?: -1, + chapterTitle = if (chapter.number == null) chapter.title else getString( + R.string.chapter_title, + chapter.number.toInt().toString(), + chapter.title + ) + ) + ) + } + val layoutManager = LinearLayoutManager(requireContext()) + binding.chapters.layoutManager = layoutManager + val itemDivider = DividerItemDecoration(requireContext(), layoutManager.orientation) + binding.chapters.addItemDecoration(itemDivider) + binding.chapters.adapter = adapter + + viewModel.chapterList.observe(viewLifecycleOwner, Observer { chapterListResponse -> + when (chapterListResponse.status) { + Status.SUCCESS -> { + chapterListResponse.data?.let { + adapter.updateChapters(it) + } + } + Status.ERROR -> Toast.makeText( + requireContext(), + "Error: ${chapterListResponse.message}", + Toast.LENGTH_SHORT + ).show() + } + }) + + } +} \ No newline at end of file diff --git a/app/src/main/java/guide/graphql/toc/ui/chapters/ChaptersViewModel.kt b/app/src/main/java/guide/graphql/toc/ui/chapters/ChaptersViewModel.kt new file mode 100644 index 0000000..a489601 --- /dev/null +++ b/app/src/main/java/guide/graphql/toc/ui/chapters/ChaptersViewModel.kt @@ -0,0 +1,41 @@ +package guide.graphql.toc.ui.chapters + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.liveData +import com.apollographql.apollo.coroutines.toDeferred +import com.apollographql.apollo.exception.ApolloException +import guide.graphql.toc.ChaptersQuery +import guide.graphql.toc.Resource +import guide.graphql.toc.apolloClient + +class ChaptersViewModel : ViewModel() { + + val bookId: LiveData = MutableLiveData(0) + + val chapterList: LiveData>> = liveData { + emit(Resource.loading(null)) + try { + val response = apolloClient.query( + ChaptersQuery() + ).toDeferred().await() + + if (response.hasErrors()) { + emit(Resource.error("Response has errors" ,null)) + return@liveData + } + response.data?.chapters?.let{ + emit(Resource.success(response.data!!.chapters)) + return@liveData + } + emit(Resource.error("Data is null" ,null)) + return@liveData + } catch (e: ApolloException) { + Log.d("ChaptersQuery", "GraphQL request failed", e) + emit(Resource.error("GraphQL request failed", null)) + return@liveData + } + } +} \ No newline at end of file diff --git a/app/src/main/java/guide/graphql/toc/SectionsAdapter.kt b/app/src/main/java/guide/graphql/toc/ui/sections/SectionsAdapter.kt similarity index 73% rename from app/src/main/java/guide/graphql/toc/SectionsAdapter.kt rename to app/src/main/java/guide/graphql/toc/ui/sections/SectionsAdapter.kt index 0323bf9..f0a9c88 100644 --- a/app/src/main/java/guide/graphql/toc/SectionsAdapter.kt +++ b/app/src/main/java/guide/graphql/toc/ui/sections/SectionsAdapter.kt @@ -1,18 +1,25 @@ -package guide.graphql.toc +package guide.graphql.toc.ui.sections import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import guide.graphql.toc.R +import guide.graphql.toc.SectionsQuery import guide.graphql.toc.databinding.SectionBinding class SectionsAdapter( - private val chapterNumber: Int?, - private val sections: List, - private val context: Context + private val context: Context, + private val chapterNumber: Int, + private var sections: List = listOf() ) : RecyclerView.Adapter() { + fun updateSections(sections: List) { + this.sections = sections + notifyDataSetChanged() + } + class ViewHolder(val binding: SectionBinding) : RecyclerView.ViewHolder(binding.root) override fun getItemCount(): Int { diff --git a/app/src/main/java/guide/graphql/toc/ui/sections/SectionsFragment.kt b/app/src/main/java/guide/graphql/toc/ui/sections/SectionsFragment.kt new file mode 100644 index 0000000..16d4f68 --- /dev/null +++ b/app/src/main/java/guide/graphql/toc/ui/sections/SectionsFragment.kt @@ -0,0 +1,93 @@ +package guide.graphql.toc.ui.sections + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.apollographql.apollo.coroutines.toDeferred +import com.apollographql.apollo.exception.ApolloException +import com.google.android.material.transition.MaterialFade +import com.google.android.material.transition.MaterialFadeThrough +import com.google.android.material.transition.MaterialSharedAxis +import guide.graphql.toc.* +import guide.graphql.toc.databinding.SectionsFragmentBinding + +class SectionsFragment : Fragment() { + + private val viewModel: SectionsViewModel by viewModels() + + private lateinit var binding: SectionsFragmentBinding + val args: SectionsFragmentArgs by navArgs() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = SectionsFragmentBinding.inflate(inflater) + return binding.root + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val forward = MaterialSharedAxis(MaterialSharedAxis.X, true) + enterTransition = forward + + val backward = MaterialSharedAxis(MaterialSharedAxis.X, false) + returnTransition = backward + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val adapter = + SectionsAdapter( + requireContext(), + args.chapterNumber + ) + val layoutManager = LinearLayoutManager(requireContext()) + binding.sections.layoutManager = layoutManager + val itemDivider = + DividerItemDecoration(requireContext(), layoutManager.orientation) + binding.sections.addItemDecoration(itemDivider) + binding.sections.adapter = adapter + + viewModel.sectionsList.observe(viewLifecycleOwner, Observer { sectionsResource -> + when (sectionsResource.status) { + Status.SUCCESS -> { + sectionsResource.data?.let { + adapter.updateSections(it) + binding.spinner.visibility = View.GONE + binding.error.visibility = View.GONE + } + } + Status.ERROR -> { + showErrorMessage(sectionsResource.message?: "") + } + Status.LOADING -> { + binding.spinner.visibility = View.VISIBLE + binding.error.visibility = View.GONE + } + } + }) + + + viewModel.chapterId = args.chapterId + + } + + private fun showErrorMessage(error: String) { + binding.spinner.visibility = View.GONE + binding.error.text = error + binding.error.visibility = View.VISIBLE + } +} \ No newline at end of file diff --git a/app/src/main/java/guide/graphql/toc/ui/sections/SectionsViewModel.kt b/app/src/main/java/guide/graphql/toc/ui/sections/SectionsViewModel.kt new file mode 100644 index 0000000..afc0a38 --- /dev/null +++ b/app/src/main/java/guide/graphql/toc/ui/sections/SectionsViewModel.kt @@ -0,0 +1,55 @@ +package guide.graphql.toc.ui.sections + +import android.util.Log +import androidx.lifecycle.* +import com.apollographql.apollo.coroutines.toDeferred +import com.apollographql.apollo.exception.ApolloException +import guide.graphql.toc.Resource +import guide.graphql.toc.SectionsQuery +import guide.graphql.toc.apolloClient + +class SectionsViewModel: ViewModel() { + + private val _chapterId: MutableLiveData = MutableLiveData() + + var chapterId: Int + get() { + return _chapterId.value ?: -1 + } + set(value) { + if (value != _chapterId.value) { + _chapterId.value = value + } + } + + val sectionsList: LiveData>> = _chapterId.switchMap { sectionId -> + return@switchMap liveData { + emit(Resource.loading(null)) + try { + val response = apolloClient.query( + SectionsQuery(id = sectionId) + ).toDeferred().await() + + if (response.hasErrors()) { + emit(Resource.error("Response has errors" ,null)) + return@liveData + } + response.data?.chapter?.sections?.let { sections -> + if (sections.size > 1) { + emit(Resource.success(sections)) + } else { + emit(Resource.error("No sections", null)) + } + return@liveData + } + emit(Resource.error("Chapter has no sections" ,null)) + return@liveData + } catch (e: ApolloException) { + Log.d("Sections Query", "GraphQL request failed", e) + emit(Resource.error("GraphQL request failed", null)) + return@liveData + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml new file mode 100644 index 0000000..a6ae313 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 20d20a1..ac96bcb 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,5 @@ - - + + + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/chapters_fragment.xml b/app/src/main/res/layout/chapters_fragment.xml index ba1e932..5c5388e 100644 --- a/app/src/main/res/layout/chapters_fragment.xml +++ b/app/src/main/res/layout/chapters_fragment.xml @@ -4,23 +4,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - - - - + app:layout_constraintTop_toTopOf="parent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/sections_fragment.xml b/app/src/main/res/layout/sections_fragment.xml index 00f805f..00425ab 100644 --- a/app/src/main/res/layout/sections_fragment.xml +++ b/app/src/main/res/layout/sections_fragment.xml @@ -5,23 +5,6 @@ android:layout_height="match_parent" android:orientation="vertical"> - - - - - - + app:layout_constraintTop_toTopOf="parent" /> + android:name="guide.graphql.toc.ui.chapters.ChaptersFragment" + tools:layout="@layout/chapters_fragment" + android:label="@string/app_name"> @@ -16,12 +17,20 @@ + android:name="guide.graphql.toc.ui.sections.SectionsFragment" + tools:layout="@layout/sections_fragment" + android:label="{chapterTitle}"> + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a85ff4c..173855b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -8,6 +8,9 @@ + +