Skip to content

Commit

Permalink
Fix part of oppia#134: Home fragment low fi with TopicList (oppia#289)
Browse files Browse the repository at this point in the history
* HomeFragment xml introduction

* Promoted Story ViewModel

* PromotedStoryViewModel

* Basic promoted story

* Promoted story data display

* Promoted card display

* GridLayoutManager

* Functional HomeFragment

* Click listeners

* Click listeners final

* Chapter name issue fixed

* Fully functional with click listeners

* Introduce ContinuePlayingActivity

* Click listener integration

* Test for ContinuePlayingActivity

* HomeTest cases

* Nit changes

* Nit suggested changes

* Code updated as per suggestions

* Added HomeItemViewModel

* Nit changes

* Orientation Change

* EOF

* Test fix
  • Loading branch information
rt4914 committed Nov 5, 2019
1 parent 47e769f commit 8bfbbec
Show file tree
Hide file tree
Showing 41 changed files with 1,322 additions and 70 deletions.
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ dependencies {
)
testImplementation(
'androidx.test:core:1.2.0',
'androidx.test.espresso:espresso-contrib:3.1.0',
'androidx.test.espresso:espresso-core:3.2.0',
'androidx.test.espresso:espresso-intents:3.1.0',
'androidx.test.ext:junit:1.1.1',
Expand All @@ -82,6 +83,7 @@ dependencies {
)
androidTestImplementation(
'androidx.test:core:1.2.0',
'androidx.test.espresso:espresso-contrib:3.1.0',
'androidx.test.espresso:espresso-core:3.2.0',
'androidx.test.espresso:espresso-intents:3.1.0',
'androidx.test.ext:junit:1.1.1',
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/OppiaTheme">
<activity android:name=".home.ContinuePlayingActivity" />
<activity android:name=".topic.conceptcard.testing.ConceptCardFragmentTestActivity" />
<activity android:name=".testing.ContentCardTestActivity" />
<activity android:name=".player.state.testing.StateFragmentTestActivity" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.appcompat.app.AppCompatActivity
import dagger.BindsInstance
import dagger.Subcomponent
import org.oppia.app.fragment.FragmentComponent
import org.oppia.app.home.ContinuePlayingActivity
import org.oppia.app.home.HomeActivity
import org.oppia.app.player.audio.testing.AudioFragmentTestActivity
import org.oppia.app.player.exploration.ExplorationActivity
Expand Down Expand Up @@ -34,9 +35,10 @@ interface ActivityComponent {
fun inject(bindableAdapterTestActivity: BindableAdapterTestActivity)
fun inject(conceptCardFragmentTestActivity: ConceptCardFragmentTestActivity)
fun inject(contentCardTestActivity: ContentCardTestActivity)
fun inject(continuePlayingActivity: ContinuePlayingActivity)
fun inject(explorationActivity: ExplorationActivity)
fun inject(homeActivity: HomeActivity)
fun inject(htmlParserTestActivty: HtmlParserTestActivity)
fun inject(htmlParserTestActivity: HtmlParserTestActivity)
fun inject(profileActivity: ProfileActivity)
fun inject(questionPlayerActivity: QuestionPlayerActivity)
fun inject(stateFragmentTestActivity: StateFragmentTestActivity)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.oppia.app.databinding

import android.graphics.drawable.GradientDrawable
import android.view.View
import androidx.annotation.ColorInt
import androidx.databinding.BindingAdapter
import org.oppia.app.R

/** Used to set a rounded-rect background drawable with a data-bound color. */
@BindingAdapter("app:roundedRectDrawableWithColor")
fun setBackgroundDrawable(view: View, @ColorInt colorRgb: Int) {
view.setBackgroundResource(R.drawable.rounded_rect_background)
// The input color needs to have alpha channel prepended to it.
(view.background as GradientDrawable).setColor((0xff000000 or colorRgb.toLong()).toInt())
}
2 changes: 2 additions & 0 deletions app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.oppia.app.fragment
import androidx.fragment.app.Fragment
import dagger.BindsInstance
import dagger.Subcomponent
import org.oppia.app.home.ContinuePlayingFragment
import org.oppia.app.home.HomeFragment
import org.oppia.app.player.exploration.ExplorationFragment
import org.oppia.app.player.state.StateFragment
Expand Down Expand Up @@ -37,6 +38,7 @@ interface FragmentComponent {
fun inject(audioFragment: AudioFragment)
fun inject(bindableAdapterTestFragment: BindableAdapterTestFragment)
fun inject(conceptCardFragment: ConceptCardFragment)
fun inject(continuePlayingFragment: ContinuePlayingFragment)
fun inject(explorationFragment: ExplorationFragment)
fun inject(homeFragment: HomeFragment)
fun inject(profileChooserFragment: ProfileChooserFragment)
Expand Down
25 changes: 25 additions & 0 deletions app/src/main/java/org/oppia/app/home/ContinuePlayingActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.oppia.app.home

import android.content.Context
import android.content.Intent
import android.os.Bundle
import org.oppia.app.activity.InjectableAppCompatActivity
import javax.inject.Inject

/** Activity for recent stories. */
class ContinuePlayingActivity : InjectableAppCompatActivity() {
@Inject lateinit var continuePlayingActivityPresenter: ContinuePlayingActivityPresenter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityComponent.inject(this)
continuePlayingActivityPresenter.handleOnCreate()
}

companion object {
/** Returns a new [Intent] to route to [ContinuePlayingActivity]. */
fun createContinuePlayingActivityIntent(context: Context): Intent {
return Intent(context, ContinuePlayingActivity::class.java)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.oppia.app.home

import androidx.appcompat.app.AppCompatActivity
import org.oppia.app.R
import org.oppia.app.activity.ActivityScope
import javax.inject.Inject

/** The presenter for [ContinuePlayingActivity]. */
@ActivityScope
class ContinuePlayingActivityPresenter @Inject constructor(private val activity: AppCompatActivity) {
fun handleOnCreate() {
activity.setContentView(R.layout.continue_playing_activity)
if (getContinuePlayingFragment() == null) {
activity.supportFragmentManager.beginTransaction().add(
R.id.continue_playing_fragment_placeholder,
ContinuePlayingFragment()
).commitNow()
}
}

private fun getContinuePlayingFragment(): ContinuePlayingFragment? {
return activity.supportFragmentManager.findFragmentById(R.id.continue_playing_fragment_placeholder) as ContinuePlayingFragment?
}
}
23 changes: 23 additions & 0 deletions app/src/main/java/org/oppia/app/home/ContinuePlayingFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.oppia.app.home

import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.oppia.app.fragment.InjectableFragment
import javax.inject.Inject

/** Fragment that contains all recently played stories. */
class ContinuePlayingFragment : InjectableFragment() {
@Inject lateinit var continuePlayingFragmentPresenter: ContinuePlayingFragmentPresenter

override fun onAttach(context: Context?) {
super.onAttach(context)
fragmentComponent.inject(this)
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return continuePlayingFragmentPresenter.handleCreateView(inflater, container)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.oppia.app.home

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.oppia.app.databinding.ContinuePlayingFragmentBinding
import org.oppia.app.fragment.FragmentScope
import javax.inject.Inject

/** The presenter for [ContinuePlayingFragment]. */
@FragmentScope
class ContinuePlayingFragmentPresenter @Inject constructor() {
fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? {
return ContinuePlayingFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false).root
}
}
10 changes: 8 additions & 2 deletions app/src/main/java/org/oppia/app/home/HomeActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@ package org.oppia.app.home
import android.os.Bundle
import org.oppia.app.activity.InjectableAppCompatActivity
import org.oppia.app.player.exploration.ExplorationActivity
import org.oppia.app.topic.TopicActivity
import javax.inject.Inject

/** The central activity for all users entering the app. */
class HomeActivity : InjectableAppCompatActivity(), RouteToExplorationListener {
@Inject lateinit var homeActivityPresenter: HomeActivityPresenter
class HomeActivity : InjectableAppCompatActivity(), RouteToTopicListener, RouteToExplorationListener {
@Inject
lateinit var homeActivityPresenter: HomeActivityPresenter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityComponent.inject(this)
homeActivityPresenter.handleOnCreate()
}

override fun routeToTopic(topicId: String) {
startActivity(TopicActivity.createTopicActivityIntent(this, topicId))
}

override fun routeToExploration(explorationId: String) {
startActivity(ExplorationActivity.createExplorationActivityIntent(this, explorationId))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.oppia.app.R
import org.oppia.app.activity.ActivityScope
import javax.inject.Inject

/** The controller for [HomeActivity]. */
/** The presenter for [HomeActivity]. */
@ActivityScope
class HomeActivityPresenter @Inject constructor(private val activity: AppCompatActivity) {
fun handleOnCreate() {
Expand Down
8 changes: 7 additions & 1 deletion app/src/main/java/org/oppia/app/home/HomeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.oppia.app.fragment.InjectableFragment
import org.oppia.app.home.topiclist.TopicSummaryClickListener
import org.oppia.app.model.TopicSummary
import javax.inject.Inject

/** Fragment that contains an introduction to the app. */
class HomeFragment : InjectableFragment() {
class HomeFragment : InjectableFragment(), TopicSummaryClickListener {
@Inject lateinit var homeFragmentPresenter: HomeFragmentPresenter

override fun onAttach(context: Context?) {
Expand All @@ -20,4 +22,8 @@ class HomeFragment : InjectableFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return homeFragmentPresenter.handleCreateView(inflater, container)
}

override fun onTopicSummaryClicked(topicSummary: TopicSummary) {
homeFragmentPresenter.onTopicSummaryClicked(topicSummary)
}
}
103 changes: 95 additions & 8 deletions app/src/main/java/org/oppia/app/home/HomeFragmentPresenter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,85 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.Transformations
import androidx.recyclerview.widget.GridLayoutManager
import org.oppia.app.databinding.HomeFragmentBinding
import org.oppia.app.fragment.FragmentScope
import org.oppia.app.home.topiclist.PromotedStoryViewModel
import org.oppia.app.home.topiclist.TopicListAdapter
import org.oppia.app.home.topiclist.TopicSummaryClickListener
import org.oppia.app.home.topiclist.TopicSummaryViewModel
import org.oppia.app.model.TopicList
import org.oppia.app.model.TopicSummary
import org.oppia.app.model.UserAppHistory
import org.oppia.app.viewmodel.ViewModelProvider
import org.oppia.domain.UserAppHistoryController
import org.oppia.domain.exploration.ExplorationDataController
import org.oppia.domain.exploration.TEST_EXPLORATION_ID_5
import org.oppia.domain.topic.TopicListController
import org.oppia.util.data.AsyncResult
import org.oppia.util.logging.Logger
import javax.inject.Inject

private const val EXPLORATION_ID = TEST_EXPLORATION_ID_5

/** The controller for [HomeFragment]. */
/** The presenter for [HomeFragment]. */
@FragmentScope
class HomeFragmentPresenter @Inject constructor(
activity: AppCompatActivity,
private val activity: AppCompatActivity,
private val fragment: Fragment,
private val viewModelProvider: ViewModelProvider<UserAppHistoryViewModel>,
private val userAppHistoryController: UserAppHistoryController,
private val topicListController: TopicListController,
private val explorationDataController: ExplorationDataController,
private val logger: Logger
) {

private val routeToExplorationListener = activity as RouteToExplorationListener
private val routeToTopicListener = activity as RouteToTopicListener

private val itemList: MutableList<HomeItemViewModel> = ArrayList()

private lateinit var topicListAdapter: TopicListAdapter

private lateinit var binding: HomeFragmentBinding

fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? {
val binding = HomeFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false)
binding = HomeFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false)
// NB: Both the view model and lifecycle owner must be set in order to correctly bind LiveData elements to
// data-bound view models.

topicListAdapter = TopicListAdapter(itemList)

val homeLayoutManager = GridLayoutManager(activity.applicationContext, 2)
homeLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if (position == 0 || position == 1) {
/* number of spaces this item should occupy = */ 2
} else {
/* number of spaces this item should occupy = */ 1
}
}
}

binding.homeRecyclerView.apply {
adapter = topicListAdapter
// https://stackoverflow.com/a/32763434/32763621
layoutManager = homeLayoutManager
}
binding.let {
it.viewModel = getUserAppHistoryViewModel()
it.presenter = this
it.lifecycleOwner = fragment
}

userAppHistoryController.markUserOpenedApp()

return binding.root
}
subscribeToUserAppHistory()
subscribeToTopicList()

private fun getUserAppHistoryViewModel(): UserAppHistoryViewModel {
return viewModelProvider.getForFragment(fragment, UserAppHistoryViewModel::class.java)
return binding.root
}

fun playExplorationButton(v: View) {
Expand All @@ -64,4 +100,55 @@ class HomeFragmentPresenter @Inject constructor(
}
})
}

private fun getUserAppHistoryViewModel(): UserAppHistoryViewModel {
return viewModelProvider.getForFragment(fragment, UserAppHistoryViewModel::class.java)
}

private val topicListSummaryResultLiveData: LiveData<AsyncResult<TopicList>> by lazy {
topicListController.getTopicList()
}

private fun subscribeToTopicList() {
getAssumedSuccessfulTopicList().observe(fragment, Observer<TopicList> { result ->

val promotedStoryViewModel = PromotedStoryViewModel(activity)
promotedStoryViewModel.setPromotedStory(result.promotedStory)
itemList.add(promotedStoryViewModel)
for (topicSummary in result.topicSummaryList) {
val topicSummaryViewModel = TopicSummaryViewModel(topicSummary, fragment as TopicSummaryClickListener)
itemList.add(topicSummaryViewModel)
}
topicListAdapter.notifyDataSetChanged()
})
}

private fun getAssumedSuccessfulTopicList(): LiveData<TopicList> {
// If there's an error loading the data, assume the default.
return Transformations.map(topicListSummaryResultLiveData) { it.getOrDefault(TopicList.getDefaultInstance()) }
}

private fun subscribeToUserAppHistory() {
getUserAppHistory().observe(fragment, Observer<UserAppHistory> { result ->
getUserAppHistoryViewModel().setAlreadyAppOpened(result.alreadyOpenedApp)
itemList.add(0, getUserAppHistoryViewModel())
topicListAdapter.notifyDataSetChanged()
})
}

private fun getUserAppHistory(): LiveData<UserAppHistory> {
// If there's an error loading the data, assume the default.
return Transformations.map(userAppHistoryController.getUserAppHistory(), ::processUserAppHistoryResult)
}

private fun processUserAppHistoryResult(appHistoryResult: AsyncResult<UserAppHistory>): UserAppHistory {
if (appHistoryResult.isFailure()) {
logger.e("HomeFragment", "Failed to retrieve user app history" + appHistoryResult.getErrorOrNull())
}
return appHistoryResult.getOrDefault(UserAppHistory.getDefaultInstance())
}

fun onTopicSummaryClicked(topicSummary: TopicSummary) {
routeToTopicListener.routeToTopic(topicSummary.topicId)
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/org/oppia/app/home/HomeItemViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.oppia.app.home

import org.oppia.app.viewmodel.ObservableViewModel

/** The root [ViewModel] for all individual items that may be displayed in home fragment recycler view. */
abstract class HomeItemViewModel: ObservableViewModel()
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.oppia.app.home

/** Listener for when an activity should route to [ContinuePlayingActivity]. */
interface RouteToContinuePlayingListener {
fun routeToContinuePlaying()
}
6 changes: 6 additions & 0 deletions app/src/main/java/org/oppia/app/home/RouteToTopicListener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.oppia.app.home

/** Listener for when an activity should route to a topic. */
interface RouteToTopicListener {
fun routeToTopic(topicId: String)
}
Loading

0 comments on commit 8bfbbec

Please sign in to comment.