Implementing pagination with Retrofit in Eventyay Attendee

Pagination (Paging) is a common and powerful technique in Android Development when making HTTP requests or fetching data from the database. Eventyay Attendee has found many situations where data binding comes in as a great solution for our network calls with Retrofit. Let’s take a look at this technique.

  • Problems without Pagination in Android Development
  • Implementing Pagination with Kotlin with Retrofit
  • Results and GIF
  • Conclusions

PROBLEMS WITHOUT DATABINDING IN ANDROID DEVELOPMENT

Making HTTP requests to fetch data from the API is a basic work in any kind of application. With the mobile application, network data usage management is an important factor that affects the loading performance of the app. Without paging, all of the data are fetched even though most of them are not displayed on the screen. Pagination is a technique to load all the data in pages of limited items, which is much more efficient

IMPLEMENTING DATABINDING IN FRAGMENT VIEW

Step 1:  Set up dependency in build.gradle

// Paging
implementation “androidx.paging:paging-runtime:$paging_version”
implementation “androidx.paging:paging-rxjava2:$paging_version”

Step 2:  Set up retrofit to fetch events from the API

@GET(“events?include=event-sub-topic,event-topic,event-type”)
fun searchEventsPaged(
  @Query(“sort”) sort: String,
  @Query(“filter”) eventName: String,
  @Query(“page[number]”) page: Int,
  @Query(“page[size]”) pageSize: Int = 5
): Single<List<Event>>

Step 3: Set up the DataSource

DataSource is a base class for loading data in the paging library from Android. In Eventyay, we use PageKeyedDataSource. It will fetch the data based on the number of pages and items per page with our default parameters. With PageKeyedDataSource, three main functions loadInitial(), loadBefore(), loadAfter() are used to to load each chunks of data.

class EventsDataSource(
  private val eventService: EventService,
  private val compositeDisposable: CompositeDisposable,
  private val query: String?,
  private val mutableProgress: MutableLiveData<Boolean>

) : PageKeyedDataSource<Int, Event>() {
  override fun loadInitial(
      params: LoadInitialParams<Int>,
      callback: LoadInitialCallback<Int, Event>
  ) {
      createObservable(1, 2, callback, null)
  }

  override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Event>) {
      val page = params.key
      createObservable(page, page + 1, null, callback)
  }

  override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Event>) {
      val page = params.key
      createObservable(page, page – 1, null, callback)
  }

  private fun createObservable(
      requestedPage: Int,
      adjacentPage: Int,
      initialCallback: LoadInitialCallback<Int, Event>?,
      callback: LoadCallback<Int, Event>?
  ) {
      compositeDisposable +=
          eventService.getEventsByLocationPaged(query, requestedPage)
              .withDefaultSchedulers()
              .subscribe({ response ->
                  if (response.isEmpty()) mutableProgress.value = false
                  initialCallback?.onResult(response, null, adjacentPage)
                  callback?.onResult(response, adjacentPage)
              }, { error ->
                  Timber.e(error, “Fail on fetching page of events”)
              }
          )
  }
}

Step 4: Set up the Data Source Factory

DataSourceFactory is the class responsible for creating DataSource object so that we can create PagedList (A type of List used for paging) for events.

class EventsDataSourceFactory(
  private val compositeDisposable: CompositeDisposable,
  private val eventService: EventService,
  private val query: String?,
  private val mutableProgress: MutableLiveData<Boolean>
) : DataSource.Factory<Int, Event>() {
  override fun create(): DataSource<Int, Event> {
      return EventsDataSource(eventService, compositeDisposable, query, mutableProgress)
  }
}

Step 5: Adapt the current change to the ViewModel. 

Previously, events fetched in List<Event> Object are now should be turned into PagedList<Event>.

sourceFactory = EventsDataSourceFactory(
  compositeDisposable,
  eventService,
  mutableSavedLocation.value,
  mutableProgress
)
val eventPagedList = RxPagedListBuilder(sourceFactory, config)
  .setFetchScheduler(Schedulers.io())
  .buildObservable()
  .cache()

compositeDisposable += eventPagedList
  .subscribeOn(Schedulers.io())
  .observeOn(AndroidSchedulers.mainThread())
  .distinctUntilChanged()
  .doOnSubscribe {
      mutableProgress.value = true
  }.subscribe({
      val currentPagedEvents = mutablePagedEvents.value
      if (currentPagedEvents == null) {
          mutablePagedEvents.value = it
      } else {
          currentPagedEvents.addAll(it)
          mutablePagedEvents.value = currentPagedEvents
      }
  }, {
      Timber.e(it, “Error fetching events”)
      mutableMessage.value = resource.getString(R.string.error_fetching_events_message)
  })

Step 6: Turn ListAdapter into PagedListAdapter

PageListAdapter is basically the same ListAdapter to update the UI of the events item but specifically used for Pagination. In here, List objects can also be null.

class EventsListAdapter : PagedListAdapter<Event, EventViewHolder>(EventsDiffCallback()) {

  var onEventClick: EventClickListener? = null
  var onFavFabClick: FavoriteFabClickListener? = null
  var onHashtagClick: EventHashTagClickListener? = null

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventViewHolder {
      val binding = ItemCardEventsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
      return EventViewHolder(binding)
  }

  override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
      val event = getItem(position)
      if (event != null)
          holder.apply {
              bind(event, position)
              eventClickListener = onEventClick
              favFabClickListener = onFavFabClick
              hashTagClickListAdapter = onHashtagClick
          }
  }

  /**
    * The function to call when the adapter has to be cleared of items
    */
  fun clear() {
      this.submitList(null)
  }

AND HERE ARE THE RESULTS…

CONCLUSION

Databinding is the way to go when working with a complex UI in Android Development. This helps reducing boilerplate code and to increase the readability of the code and the performance of the UI. One problem with data-binding is that sometimes, it is pretty hard to debug with unhelpful log messages. Hopefully, you can empower your UI in your project now with data-binding. 

Pagination is the way to go for fetching items from the API and making infinite scrolling. This helps reduce network usage and improve the performance of Android applications. And that’s it. I hope you can make your application more powerful with pagination. 

RESOURCES

Open Event Codebase: https://github.com/fossasia/open-event-attendee-android/pull/2012

Documentation: https://developer.android.com/topic/libraries/architecture/paging/ 

Google Codelab: https://codelabs.developers.google.com/codelabs/android-paging/#0

Continue Reading

Handle app links and apply unit tests in Eventyay attendee Android application

The eventyay attendee is an android app which allows users to discover events happening around the world using the Open Event Platform. It consumes the APIs of the open event server to get a list of available events and can get detailed information about them. Users following links on devices have one goal in mind: to get to the content they want to see. As a developer, you can set up Android App Links to take users to a link’s specific content directly in your app, bypassing the app-selection dialog, also known as the disambiguation dialog. Because Android App Links leverage HTTP URLs and association with a website, users who don’t have your app installed go directly to content on your site.

A unit test generally exercises the functionality of the smallest possible unit of code (which could be a method, class, or component) in a repeatable way. You should build unit tests when you need to verify the logic of specific code in your app.

  • Why unit test cases?
  • Setup app link intent in the app
  • Apply unit test cases
  • Conclusion
  • Resources

Let’s analyze every step in detail.

Why unit test cases?

As it is already discussed what the app link intents are, and it is basically for providing a better user experience for the application. These are some following reason why unit test cases should use – 

  1. Rapid feedback on failures.
  2. Early failure detection in the development cycle.
  3. Safer code refactoring, letting you optimize code without worrying about regressions.
  4. Stable development velocity, helping you minimize technical debt.

JUnit4 library is used for unit tests.

Setup app link intent in the app

Declare the frontend host according to build flavor in the app level gradle file:

buildTypes {
        release {
            resValue “string”,  “FRONTEND_HOST”, “eventyay.com”
        }
        debug {
            resValue “string”, “FRONTEND_HOST”, “open-event-fe.netlify.com”
        }
    }

Handle the app link intent in Manifest file by adding intent filter under main activity decleartion:

<intent-filter>
    <action android:name=”android.intent.action.VIEW” />
    <category android:name=”android.intent.category.DEFAULT” />
    <category android:name=”android.intent.category.BROWSABLE” />

    <data
        android:scheme=”https”
        android:host=”@string/FRONTEND_HOST”/>
</intent-filter>

Manifest will through the intent in the main activity file.

Now handle the intent in main activity:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        handleAppLinkIntent(intent)
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        handleAppLinkIntent(intent)
    }

    private fun handleAppLinkIntent(intent: Intent?) {
        val uri = intent?.data ?: return
        val bundle = AppLinkUtils.getArguments(uri)
        val destinationId = AppLinkUtils.getDestinationId(uri)
        if (destinationId != null) {
            navController.navigate(destinationId, bundle)
        }
    }

Here a new class/object AppLinkUtils is defined which will return destination fragment id and the argument/data according to the intent URI.

Apply unit test cases:

First, implement the libraries in the gradle file –  1. JUnit for unit tests, 2. Robolectric for using android classes in the test class:

testImplementation ‘junit:junit:4.12’
testImplementation ‘org.robolectric:robolectric:3.4.2’

Create a test class for testing the app link functions and run it with RoboLectricTestRunner:

private const val EVENT = “event”
private const val RESET_PASSWORD = “resetPassword”
private const val VERIFY_EMAIL = “verifyEmail”

@RunWith(RobolectricTestRunner::class)
class AppLinkUtilsTest {

    private fun getAppLink(type: String): Uri {
        return when (type) {
            EVENT -> Uri.parse(“https://eventyay.com/e/5f6d3feb”)
            RESET_PASSWORD -> Uri.parse(“https://eventyay.com/reset-password?token=822980340478781748445098077144”)
            VERIFY_EMAIL -> Uri.parse(“https://eventyay.com/verify?token=WyJsaXZlLmhhcnNoaXRAaG”)
            else -> Uri.parse(“”)
        }
    }

    @Test
    fun `should get event link`() {
        val uri = getAppLink(EVENT)
        assertEquals(R.id.eventDetailsFragment, AppLinkUtils.getDestinationId(uri))
        assertEquals(“””
            5f6d3feb
        “””.trimIndent(), AppLinkUtils.getArguments(uri).getString(EVENT_IDENTIFIER))
    }// Find more test cases in the GitHub Repo.

Testing response:

GIF

In a Nutshell

So, essentially the Eventyay Attendee should have this feature to handle all links i.e. Reset password, verify user email and open event details in the app itself. So, we can provide a better user experience in-app instead of redirecting to the frontend for them.

Resources

  1. Android app links: https://developer.android.com/studio/write/app-link-indexing
  2. Developing android unit testing: https://www.vogella.com/tutorials/AndroidTesting/article.html

Tags

Eventyay, open-event, JUnit, AndroidUnitTest, AppLinks, Fossasia, GSoC, Android, Kotlin

Continue Reading

Configure location feature using MVVM in Eventyay Attendee Application

The eventyay attendee is an android app which allows users to discover events happening around the world using the Open Event Platform. It consumes the APIs of the open event server to get a list of available events and can get detailed information about them. It deals with events based on location, but we have to take the location as an input from the user. While in many cases, we have to search for events on our current location only. To make this work, I have added a current location option, where the app will get our location and search for nearby events. Earlier we had to enter our current location as well to search nearby events.

Model–view–viewmodel is a software architectural pattern. MVVM facilitates separation of development of the graphical user interface – be it via a markup language or GUI code – from the development of the business logic or back-end logic (the data model).

  • Why Model-view-ViewModel?
  • Setup Geo location View Model
  • Configure location feature with MVVM
  • Conclusion
  • Resources

Let’s analyze every step in detail.

Advantages of using Model-view-ViewModel

  1. A clean separation of different kinds of code should make it easier to go into one or several of those more granular and focused parts and make changes without worrying.
  2. External and internal dependencies are in separate pieces of code from the parts with the core logic that you would like to test.
  3. Observation of mutable live data whenever it is changed.

Setup the Geolocation view model

Created new kotlin class name GeoLocationViewModel which contains a configure function for current location:

package org.fossasia.openevent.general.search

class GeoLocationViewModel : ViewModel() {
    fun configure(activity: Activity) { }                                    
}

The GeoLocationViewModel class implement as VIewModel().

Now add the class in module inside Modules.kt :

val viewModelModule = module {
    viewModel { GeoLocationViewModel() }
}

Configure location feature with GeoLocationViewModel:

First, add play store location service implementation inside dependencies of build.gradle:

// Location Play Service
playStoreImplementation ‘com.google.android.gms:play-services-location:16.0.0’

Now we need location permissions to implement this feature. Adding user permissions in the manifest file:

<uses-permission android:name=”android.permission.INTERNET” />
<uses-permission android:name=”android.permission.ACCESS_WIFI_STATE” />

Now ask user for the location permission:

private fun checkLocationPermission() {
        val permission = context?.let {
            ContextCompat.checkSelfPermission(it, Manifest.permission.ACCESS_COARSE_LOCATION) }
        if (permission != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION,
                Manifest.permission.ACCESS_FINE_LOCATION), LOCATION_PERMISSION_REQUEST)
        }
    }

Check for device location is enabled, if not send an intent to turn location on. The method is written inside configure function:

val service = activity.getSystemService(Context.LOCATION_SERVICE)
        var enabled = false
        if (service is LocationManager) enabled = service.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
        if (!enabled) {
            val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
            activity.startActivity(intent)
            return
        }

Now create Mutable live data for current location inside the view model class :

private val mutableLocation = MutableLiveData<String>()
val location: LiveData<String> = mutableLocation

Now implement location request and location callback inside configure method:

val locationRequest: LocationRequest = LocationRequest.create()
locationRequest.priority = LocationRequest.PRIORITY_LOW_POWER
val locationCallback = object : LocationCallback() {
      override fun onLocationResult(locationResult: LocationResult?) {
          if (locationResult == null) {
              return
            }
            for (location in locationResult.locations) {
                if (location != null) {
                    val latitude = location.latitude
                    val longitude = location.longitude
                    try {
                        val geocoder = Geocoder(activity, Locale.getDefault())
                        val addresses: List<Address> = geocoder.getFromLocation(latitude, longitude, maxResults)
                        for (address: Address in addresses) {
                            if (address.adminArea != null) {
                                mutableLocation.value = address.adminArea
                            }
                        }
                    } catch (exception: IOException) {
                        Timber.e(exception, “Error Fetching Location”)
                    }
                }
            }
        }
    }

Now call location service inside configure method:

LocationServices
                .getFusedLocationProviderClient(activity)
                .requestLocationUpdates(locationRequest, locationCallback, null)

Now observe data for current location inside the fragment from the view model. Save the current user location and go to the main activity:

private val geoLocationViewModel by viewModel<GeoLocationViewModel>()
geoLocationViewModel.location.observe(this, Observer { location ->
                searchLocationViewModel.saveSearch(location)
                redirectToMain()
            })

GIF

In a Nutshell

So, essentially the Eventyay Attendee should have this feature to show all the events nearby me or in my city, although the app was already doing the job, but we had to manually select the city or locality we wish to search, now after the addition of dedicated current location option, the app will be more user friendly and automated.

Resources

  1. Google location services API: https://www.toptal.com/android/android-developers-guide-to-google-location-services-api
  2. MVVM architecture in Android: https://proandroiddev.com/mvvm-architecture-viewmodel-and-livedata-part-1-604f50cda1

Tags

Eventyay, open-event, PlayServices, Location, MVVM, Fossasia, GSoC, Android, Kotlin

Continue Reading

Data Binding with Kotlin in Eventyay Attendee

Databinding is a common and powerful technique in Android Development. Eventyay Attendee has found many situations where data binding comes in as a great solution for our complex UI. Let’s take a look at this technique.

  • Problems without data binding in Android Development
  • Implementing Databinding with Kotlin inside Fragment
  • Implementing Databinding with Kotlin inside RecyclerView/Adapter
  • Results and GIF
  • Conclusions

PROBLEMS WITHOUT DATABINDING IN ANDROID DEVELOPMENT

Getting the data and fetching it to the UI is a basic work in any kind of application. With Android Development, the most common way to do is it to call function like .setText(), isVisible = True/False,.. in your fragment. This can create many long boilerplate codes inside Android classes. Databinding removes them and moves to the UI classes (XML).

IMPLEMENTING DATABINDING IN FRAGMENT VIEW

Step 1: Enabling data binding in the project build.gradle

android {
  dataBinding {
      enabled = true
  }

Step 2: Wrap the current layout with <layout></layout> tag. Inside that, put <data></data> to indicate any variables needed for data binding. For example, this code here display an event variable for our fragment about event details:

<layout xmlns:android=”http://schemas.android.com/apk/res/android”
  xmlns:app=”http://schemas.android.com/apk/res-auto”
  xmlns:bind=”http://schemas.android.com/tools”>

  <data>

      <variable
          name=”event”
          type=”org.fossasia.openevent.general.event.Event” />
  </data>

  <androidx.coordinatorlayout.widget.CoordinatorLayout
      android:id=”@+id/eventCoordinatorLayout”
      android:layout_width=”match_parent”
      android:layout_height=”match_parent”
      android:background=”@android:color/white”>

Step 3: Bind your data in the XML file and create a Binding Adapter class for better usage

With the setup above, you can start binding your data with “@{<data code here>}”

<TextView
  android:id=”@+id/eventName”
  android:layout_width=”0dp”
  android:layout_height=”wrap_content”
  android:layout_marginLeft=”@dimen/layout_margin_large”
  android:layout_marginTop=”@dimen/layout_margin_large”
  android:layout_marginRight=”@dimen/layout_margin_large”
  android:text=”@{event.name}”
  android:fontFamily=”sans-serif-light”
  android:textColor=”@color/dark_grey”
  android:textSize=”@dimen/text_size_extra_large”
  app:layout_constraintEnd_toEndOf=”@+id/eventImage”
  app:layout_constraintStart_toStartOf=”@+id/eventImage”
  app:layout_constraintTop_toBottomOf=”@+id/eventImage”
  tools:text=”Open Source Meetup” />

Sometimes, to bind our data normally we need to use a complex function, then creating Binding Adapter class really helps. For example, Eventyay Attendee heavily uses Picasso function to fetch image to ImageView:

@BindingAdapter(“eventImage”)
fun setEventImage(imageView: ImageView, url: String?) {
  Picasso.get()
      .load(url)
      .placeholder(R.drawable.header)
      .into(imageView)
}
<ImageView
  android:id=”@+id/eventImage”
  android:layout_width=”@dimen/layout_margin_none”
  android:layout_height=”@dimen/layout_margin_none”
  android:scaleType=”centerCrop”
  android:transitionName=”eventDetailImage”
  app:layout_constraintDimensionRatio=”2″
  app:layout_constraintEnd_toEndOf=”parent”
  app:layout_constraintHorizontal_bias=”0.5″
  app:layout_constraintStart_toStartOf=”parent”
  app:eventImage=”@{event.originalImageUrl}”
  app:layout_constraintTop_toBottomOf=”@id/alreadyRegisteredLayout” />

Step 4: Finalize data binding setup in Android classes. We can create a binding variable. The binding root will serve as the root node of the layout. Whenever data is needed to be bind, set the data variable stated to that binding variable and call function executePendingBingdings()

private lateinit var rootView: View
private lateinit var binding: FragmentEventBinding
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_event, container, false)
rootView = binding.root
binding.event = event
binding.executePendingBindings()

SOME NOTES

  • In the example mentioned above, the name of the binding variable class is auto-generated based on the name of XML file + “Binding”. For example, the XML name was fragment_event so the DataBinding classes generated name is FragmentEventBinding.
  • The data binding class is only generated only after compiling the project.
  • Sometimes, compiling the project fails because of some problems due to data binding without any clear log messages, then that’s probably because of error when binding your data in XML class. For example, we encounter a problem when changing the value in Attendee data class from firstname to firstName but XML doesn’t follow the update. So make sure you bind your data correctly
<TextView
  android:id=”@+id/name”
  android:layout_width=”wrap_content”
  android:layout_height=”wrap_content”
  android:layout_marginBottom=”@dimen/layout_margin_large”
  android:textColor=”@color/black”
  android:textSize=”@dimen/text_size_expanded_title_large”
  android:text=”@{attendee.firstname + ` ` + attendee.lastname}”
  tools:text=”@string/name_preview” />

CONCLUSION

Databinding is the way to go when working with a complex UI in Android Development. This helps reducing boilerplate code and to increase the readability of the code and the performance of the UI. One problem with data binding is that sometimes, it is pretty hard to debug with an unhelpful log message. Hopefully, you can empower your UI in your project now with data binding.  

RESOURCES

Eventyay Attendee Android Codebase: https://github.com/fossasia/open-event-android

Eventyay Attendee Android PR: #1961 – feat: Set up data binding for Recycler/Adapter

Documentation: https://developer.android.com/topic/libraries/data-binding

Google Codelab: https://codelabs.developers.google.com/codelabs/android-databinding/#0

Continue Reading

Dependency Injection with Kotlin Koin in Eventyay Attendee

Eventyay Attendee Android app contains a lot of shared components between classes that should be reused. Dependency Injection with Koin really comes in as a great problem solver.

Dependency Injection is a common design pattern used in various projects, especially with Android Development. In short, dependency injection helps to create/provide instances to the dependent class, and share it among other classes.

  • Why using Koin?
  • Process of setting up Koin in the application
  • Results
  • Conclusion
  • Resources

Let’s get into the details

WHY USING KOIN?

Before Koin, dependency injection in Android Development was mainly used with other support libraries like Dagger or Guice. Koin is a lightweight alternative that was developed for Kotlin developers. Here are some of the major things that Koin can do for your project:

  • Modularizing your project by declaring modules
  • Injecting class instances into Android classes
  • Injecting class instance by the constructor
  • Supporting with Android Architecture Component and Kotlin
  • Testing easily

SETTING UP KOIN IN THE ANDROID APPLICATION

Adding the dependencies to build.gradle

// Koin
implementation “org.koin:koin-android:$koin_version”
implementation “org.koin:koin-androidx-scope:$koin_version”
implementation “org.koin:koin-androidx-viewmodel:$koin_version”

Create a folder to manage all the dependent classes.

Inside this Modules class, we define modules and create “dependency” class instances/singletons that can be reused or injected. For Eventyay Attendee, we define 5 modules: commonModule, apiModule, viewModelModule, networkModule, databaseModule. This saves a lot of time as we can make changes like adding/removing/editing the dependency in one place.

Let’s take a look at what is inside some of the modules:

DatabaseModule

val databaseModule = module {

  single {
      Room.databaseBuilder(androidApplication(),
          OpenEventDatabase::class.java, “open_event_database”)
          .fallbackToDestructiveMigration()
          .build()
  }

  factory {
      val database: OpenEventDatabase = get()
      database.eventDao()
  }

  factory {
      val database: OpenEventDatabase = get()
      database.sessionDao()
  }

CommonModule

val commonModule = module {
  single { Preference() }
  single { Network() }
  single { Resource() }
  factory { MutableConnectionLiveData() }
  factory<LocationService> { LocationServiceImpl(androidContext()) }
}

ApiModule

val apiModule = module {
  single {
      val retrofit: Retrofit = get()
      retrofit.create(EventApi::class.java)
  }
  single {
      val retrofit: Retrofit = get()
      retrofit.create(AuthApi::class.java)
  }

NetworkModule

single {
  val connectTimeout = 15 // 15s
  val readTimeout = 15 // 15s

  val builder = OkHttpClient().newBuilder()
      .connectTimeout(connectTimeout.toLong(), TimeUnit.SECONDS)
      .readTimeout(readTimeout.toLong(), TimeUnit.SECONDS)
      .addInterceptor(HostSelectionInterceptor(get()))
      .addInterceptor(RequestAuthenticator(get()))
      .addNetworkInterceptor(StethoInterceptor())

  if (BuildConfig.DEBUG) {
      val httpLoggingInterceptor = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
      builder.addInterceptor(httpLoggingInterceptor)
  }
  builder.build()
}

single {
  val baseUrl = BuildConfig.DEFAULT_BASE_URL
  val objectMapper: ObjectMapper = get()
  val onlineApiResourceConverter = ResourceConverter(
      objectMapper, Event::class.java, User::class.java,
      SignUp::class.java, Ticket::class.java, SocialLink::class.java, EventId::class.java,
      EventTopic::class.java, Attendee::class.java, TicketId::class.java, Order::class.java,
      AttendeeId::class.java, Charge::class.java, Paypal::class.java, ConfirmOrder::class.java,
      CustomForm::class.java, EventLocation::class.java, EventType::class.java,
      EventSubTopic::class.java, Feedback::class.java, Speaker::class.java, FavoriteEvent::class.java,
      Session::class.java, SessionType::class.java, MicroLocation::class.java, SpeakersCall::class.java,
      Sponsor::class.java, EventFAQ::class.java, Notification::class.java, Track::class.java,
      DiscountCode::class.java, Settings::class.java, Proposal::class.java)

  Retrofit.Builder()
      .client(get())
      .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
      .addConverterFactory(JSONAPIConverterFactory(onlineApiResourceConverter))
      .addConverterFactory(JacksonConverterFactory.create(objectMapper))
      .baseUrl(baseUrl)
      .build()
}

As described in the code, Koin support single for creating a singleton object, factory for creating a new instance every time an object is injected.

With all the modules created, it is really simple to get Koin running in the project with the function startKoin() and a few lines of code. We use it inside the application class:

startKoin {
  androidLogger()
  androidContext([email protected])
  modules(listOf(
      commonModule,
      apiModule,
      viewModelModule,
      networkModule,
      databaseModule
  ))
}

Injecting created instances defined in the modules can be used in two way, directly inside a constructor or injecting into Android classes.  

Here is an example of dependency injection to the constructor that we used for a ViewModel class and injecting that ViewModel class into the Fragment:

class EventsViewModel(
  private val eventService: EventService,
  private val preference: Preference,
  private val resource: Resource,
  private val mutableConnectionLiveData: MutableConnectionLiveData,
  private val config: PagedList.Config,
  private val authHolder: AuthHolder
) : ViewModel() {
class EventsFragment : Fragment(), BottomIconDoubleClick {
  private val eventsViewModel by viewModel<EventsViewModel>()
  private val startupViewModel by viewModel<StartupViewModel>()

For testing, it is also really easy with support library from Koin.

@Test
fun testDependencies() {
  koinApplication {
      androidContext(mock(Application::class.java))
      modules(listOf(commonModule, apiModule, databaseModule, networkModule, viewModelModule))
  }.checkModules()
}

CONCLUSION

Koin is really easy to use and integrate into Kotlin Android project. Apart from some of the basic functionalities mention above, Koin also supports other helpful features like Scoping or Logging with well-written documentation and examples. Even though it is only developed a short time ago, Koin has proved to be a great use in the Android community. So the more complicated your project is, the more likely it is that dependency injection with Koin will be a good idea.

RESOURCES 

Documentation: https://insert-koin.io/

Eventyay Attendee Android Codebase: https://github.com/fossasia/open-event-android

Continue Reading

Correct the API for Downloading GitHub Content in SUSI.AI android app

The content from github in the SUSI.AI android app is downloaded through simple links and the data is parsed through them and is used in the app depending upon what needs to be done with that data at that time.

A simple example for this that was used in the app was  :

private val imageLink = “https://raw.githubusercontent.com/fossasia/susi_skill_data/master/models/general/”

Above is the link that is used to download and display the images of the skills in the app. All the api calls that generally take place in SUSI are through the SUSI server, and making the call to display the images for the skills which takes place through the github links should be replaced by making the calls to the SUSI server instead, as this is a terrible programming style, with this style the project cannot be cloned from other developers and it cannot be moved to other repositories.

We see that there was a huge programming style issue present in the android app and hence, it was fixed by adding the API that calls the SUSI server for external source images and removing the existing implementation that downloads the image from github directly.

Below is an example of the link to the API call made in the app that was needed for the request to be made to the SUSI server :

${BaseUrl.SUSI_DEFAULT_BASE_URL}/cms/getImage.png?model=${skillData.model}&language=${skillData.language}&group=${skillData.group}&image=${skillData.image}

The link is displayed in the kotlin string interpolation manner, here is what the actual URL would look like :

https://api.susi.ai/cms/getImage.pngmodel=${skillData.model}&language=${skillData.language}&group=${skillData.group}&image=${skillData.image}

Here the values with ‘$’ symbol are the parameters for the API taken from the SkillData.kt file and are put inside the link so that the image needed can be extracted.

Now, since we use this link to set the images, to avoid the duplicate code, an Object class was made for this purpose. The object class contained two functions, one for setting the image and one for parsing the skilldata object and forming a URL out of it. Here is the code for the object class :

object Utils {

  fun setSkillsImage(skillData: SkillData, imageView: ImageView) {
      Picasso.with(imageView.context)
              .load(getImageLink(skillData))
              .error(R.drawable.ic_susi)
              .fit()
              .centerCrop()
              .into(imageView)
  }

  fun getImageLink(skillData: SkillData): String {
      val link = “${BaseUrl.SUSI_DEFAULT_BASE_URL}/cms/getImage.png?model=${skillData.model}&language=${skillData.language}&group=${skillData.group}&image=${skillData.image}”
              .replace(” “,“%20”)
      Timber.d(“SUSI URI” + link)
      return link
  }
}

setSkillsImage() method sets the image in the ImageView and the getImageLink() method returns the image formed out of the SkillData object.

References

 

Continue Reading

Handle response for API calls to the smart speaker

The SUSI.AI android app has the feature to connect to the smart speaker through the WiFi access point of the smart speaker. This connection takes place without internet and is completely offline and takes place locally between the device and the app.

When the APIs are hit, the parameters are sent to the server and are extracted by the speaker and then stored to perform further actions, but like any other API there is some response sent by the server once the parameters are fetched and only when this response is correctly parsed we show the confirmation that the data was required to be sent to the speaker is successfully done.

The SUSI.AI android app was unable to parse this response and so , every time there was a request made to the speaker the error message was displayed on the screen, just like this :

This is a cropped screenshot of the error message shown.

To handle the response from the local server there was a need to add the response parsing classes in the app, also since the app is written in kotlin the specialised classes for this purpose are already present in kotlin which are specifically for the purpose of handling responses. The data classes are added for the three APIs for the local server that are made, the response class added for the WiFi credentials response is :

data class SpeakerWifiResponse(
      val wifi: String,
      val wifi_ssid: String,
      val wifi_password: String
)

Here the first string by the name of ‘wifi’ is used to display a success message and other two variables are for the ssid and password of the wifi network respectively.

The API that is used to set the configuration of the speaker has the response class as follows  :

data class SpeakerConfigResponse(
      val configuration: String,
      val hotword: String,
      val stt: String,
      val tts: String,
      val wake: String
)

The third API which is used to send the auth credentials to the speaker has the following response class :

data class SpeakerAuthResponse(
      val auth: String,
      val authentication: String,
      val email: String,
      val password: String? = null
)

Now these response classes have the variable name similar to the key values in the JSON response generated by the local server. Due to the similar names to the keys an extra step and extra lines of code are eliminated which had to be added otherwise denoting the “Serializable” values to match the JSON response values.

After these data classes were added the Call<> functions of the retrofit service DeviceApi.java were made of the type of the types of the above data classes , which then were able to successfully parse the response from the local server of the speaker.

So, after this the app to speaker connection was successful and a success message was shown as :

References

 

Continue Reading

Ticket Details in the Open Event Android App

After entering all the attendee details and buying a ticket for an event the user expects to see the ticket so that he can use it later. This is why ticket details are shown in a separate fragment in the Open Event Android App. Let’s see how the tickets fragment was made in the Open Event Android App.

Two things that we require from the previous fragment are the event id and the order identifier so that we show the information related to the event as well as the order.

if (bundle != null) {
id = bundle.getLong(EVENT_ID, -1)
orderId = bundle.getString(ORDERS)
}

 

We are requesting data from the following two endpoints. In the first GET request we are passing the order identifier in the URL and we get the list of attendees from the server. In the second endpoint we simply pass the event identifier and get the event details from the server.

 

@GET("/v1/orders/{orderIdentifier}/attendees")
fun attendeesUnderOrder(@Path("orderIdentifier") orderIdentifier: String): Single<List<Attendee>>

@GET("/v1/events/{eventIdentifier}")
fun getEventFromApi(@Path("eventIdentifier") eventIdentifier: Long): Single<Event>

 

Here we are observing the attendees live data and adding the list of attendees returned from the server to the recyclerview so that we can show the user all the details of the attendees like the first name, last name etc. We then notify the adapter that the list of attendees have been added. In the end we log the number of attendees so that it is easier to debug in case there are any bugs.

orderDetailsViewModel.attendees.observe(this, Observer {
it?.let {
ordersRecyclerAdapter.addAll(it)
ordersRecyclerAdapter.notifyDataSetChanged()
}
Timber.d("Fetched attendees of size %s", ordersRecyclerAdapter.itemCount)
})

 

As mentioned earlier we need the event id and order identifier to show event and attendee related information to the user so here we are using the event id and appending it to the url. We are sending a GET request in a background thread and storing the list of events returned from the server in a mutable live data. In case of any errors we log it and show the error message to the user. Similarly we will use the order identifier to get the list of orders from the server and show it to the user.

compositeDisposable.add(eventService.getEventFromApi(id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
event.value = it
}, {
Timber.e(it, "Error fetching event %d", id)
message.value = "Error fetching event"
}))

 

After fetching the list of attendees and event details, the only thing that we need to do is extract the important information and show it to the user so we pass the order and event objects to the ViewHolder. This can be done simply by using the attendee and event objects and accessing the fields required.

itemView.name.text = "${attendee.firstname} ${attendee.lastname}"
itemView.eventName.text = event?.name
itemView.date.text = "$formattedDate\n$formattedTime $timezone"

 

Resources

  1. ReactiveX official documentation: http://reactivex.io/
  2. Retrofit Android: http://square.github.io/retrofit/
  3. Google Android Developers Recycler View: https://developer.android.com/guide/topics/ui/layout/recyclerview
Continue Reading

Attendee details in the Open Event Android App

To be able to create an order we first need to create an attendee with whom we can associate an order. Let’s see how in Open Event Android App we are creating an attendee.

We are loading the event details from our local database using the id variable. Since only logged in users can create an attendee, if the user is not logged in then the user is redirected to the login screen. If any errors are encountered while creating an attendee then they are shown in a toast message to the user. When the user clicks on the register button a POST request is sent to the server with the necessary details of the attendee. In the POST request we are passing an attendee object which has the id, first name, last name and email of the attendee. The ticket id and event id is also sent.

attendeeFragmentViewModel.loadEvent(id)

if (!attendeeFragmentViewModel.isLoggedIn()) {
redirectToLogin()
Toast.makeText(context, "You need to log in first!", Toast.LENGTH_LONG).show()
}

 

attendeeFragmentViewModel.message.observe(this, Observer {
Toast.makeText(context, it, Toast.LENGTH_LONG).show()
})

attendeeFragmentViewModel.progress.observe(this, Observer {
it?.let { Utils.showProgressBar(rootView.progressBarAttendee, it) }
})

 

attendeeFragmentViewModel.event.observe(this, Observer {
it?.let { loadEventDetails(it) }
})

rootView.register.setOnClickListener {
val attendee = Attendee(id = attendeeFragmentViewModel.getId(),
firstname = firstName.text.toString(),
lastname = lastName.text.toString(),
email = email.text.toString(),
ticket = ticketId,
event = eventId)

attendeeFragmentViewModel.createAttendee(attendee)

 

We are using a method called loadEvent in the above code which is defined in the View Model let’s have a look. We are throwing an IllegalStateException if the id is equal to -1 because this should never happen. Then we are fetching the event from the database in a background thread. If we face any errors while fetching the event we report it to the user

 

fun loadEvent(id: Long) {
if (id.equals(-1)) {
throw IllegalStateException("ID should never be -1")
}
compositeDisposable.add(eventService.getEvent(id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
event.value = it
}, {
Timber.e(it, "Error fetching event %d", id)
message.value = "Error fetching event"
}))
}

 

This method is used to create an attendee. We are checking if the user has filled all the fields if any of the fields is empty a toast message is shown. Then we send a POST request to the server in a background thread. The progress bar starts loading as soon as the request is made and then finally when the attendee has been created successfully, the progress bar stops loading and a success message is shown to the user. If we face any errors while creating an attendee, an error message is shown to the user.

fun createAttendee(attendee: Attendee) {
if (attendee.email.isNullOrEmpty() || attendee.firstname.isNullOrEmpty() || attendee.lastname.isNullOrEmpty()) {
message.value = "Please fill all the fields"
return
}

compositeDisposable.add(attendeeService.postAttendee(attendee)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {
progress.value = true
}.doFinally {
progress.value = false
}.subscribe({
message.value = "Attendee created successfully!"
Timber.d("Success!")
}, {
message.value = "Unable to create Attendee!"
Timber.d(it, "Failed")
}))
}

 

This function sends a POST request to the server and stores the attendee details in the local database.

fun postAttendee(attendee: Attendee): Single<Attendee> {
return attendeeApi.postAttendee(attendee)
.map {
attendeeDao.insertAttendee(it)
it
}

 

This is how the attendee details are inserted into the local database. In case of a conflict the attendee object gets replaced.

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAttendee(attendee: Attendee)

 

Resources

  1. ReactiveX official documentation : http://reactivex.io/
  2. Vogella RxJava 2 – Tutorial : http://www.vogella.com/tutorials/RxJava/article.html
  3. Androidhive RxJava Tutorial : https://www.androidhive.info/RxJava/
Continue Reading

Handling No internet cases in Open Event Android

It’s pretty common to face connectivity issues and when the user has no Internet connection he should be shown an appropriate response rather than allowing him to send requests to the server. Let’s have a look how we are handling such cases in Open Event Android

Firstly we need to add the required permission in the manifest. We need the permission to access the user’s WiFi state and network state.

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

 

We use this function to check if the user is connected to the Internet. This function return a Boolean which is true if the user is connected to the Internet otherwise it is false

private fun isNetworkConnected(): Boolean {
val connectivityManager = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager

return connectivityManager?.activeNetworkInfo != null
}

 

This function is used to decide which screen should be shown to the user. If the user has an active Internet connection he will see events fragment but if there is no Internet he will see the no Internet card.

private fun showNoInternetScreen(show: Boolean) {
rootView.homeScreenLL.visibility = if (show) View.VISIBLE else View.GONE
rootView.noInternetCard.visibility = if (!show) View.VISIBLE else View.GONE
}

 

Let’s see how the above two functions are used in the events fragment. When the app starts we check if there is a need to show the no Internet screen. If the user is not connected to the Internet, the no Internet card will be shown. Then when the user clicks on retry, the events fragment is shown again if the user is connected to the Internet.

showNoInternetScreen(isNetworkConnected())

rootView.retry.setOnClickListener {
showNoInternetScreen(isNetworkConnected())
}

 

Let’s have a look a how the XML code looks, here we are only seeing a part of the code as the rest is pretty obvious. We have cardView and inside it all the views ie ImageView,TextView are inside a LinearLayout which has a vertical orientation so that all these views appear below each other.

<android.support.v7.widget.CardView
android:id="@+id/noInternetCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_margin_medium"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="@dimen/card_corner_radius"
app:cardElevation="@dimen/card_elevation">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_margin_extra_large"
android:orientation="vertical">

<ImageView
android:id="@+id/noInternetImageView"
android:layout_width="@dimen/item_image_view_large"
android:layout_height="@dimen/item_image_view_large"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/layout_margin_large"
android:scaleType="centerCrop"
app:srcCompat="@drawable/ic_no_internet" />

<TextView
android:id="@+id/noInternetTextview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/layout_margin_large"
android:text="@string/no_internet_message"
android:textSize="@dimen/text_size_medium"
tools:text="No Internet" />

 

References

  1. AndroidHive tutorial – https://www.androidhive.info/2012/07/android-detect-internet-connection-status/
  2. Official Android Documentation – https://developer.android.com/training/monitoring-device-state/connectivity-monitoring
  3. StackOverflow – https://stackoverflow.com/questions/9570237/android-check-internet-connection
Continue Reading
  • 1
  • 2
Close Menu