Adding time counter on ordering tickets in Eventyay Attendee

In Eventyay Attendee, ordering tickets for events has always been a core functionality that we focus on. When ordering tickets, adding a time counter to make a reservation and release tickets after timeout is a common way to help organizers control their tickets’ distribution and help users save up their tickets. Let’s take a look at how to implement this feature

  • Implementing the time counter 
  • Some notes on implementing time counter
  • Conclusion
  • Resources

INTEGRATING TIME COUNTER TO YOUR SYSTEM

Step 1: Create the UI for your time counter. In here, we made a simple View container with TextView inside to update the time.

Step 2: Set up the time counter with Android CountdownTimer with the total countdown time and the ticking time. In Eventyay, the default countdown time is 10 minutes (600,000 ms) with the ticking time is (1,000 ms), which means the UI is updated every one second.

private fun setupCountDownTimer(orderExpiryTime: Int) {
  rootView.timeoutCounterLayout.isVisible = true
  rootView.timeoutInfoTextView.text =
      getString(R.string.ticket_timeout_info_message, orderExpiryTime.toString())

  val timeLeft: Long = if (attendeeViewModel.timeout == -1L) orderExpiryTime * 60 * 1000L
                          else attendeeViewModel.timeout
  timer = object : CountDownTimer(timeLeft, 1000) {
      override fun onFinish() {
          findNavController(rootView).navigate(AttendeeFragmentDirections
              .actionAttendeeToTicketPop(safeArgs.eventId, safeArgs.currency, true))
      }

      override fun onTick(millisUntilFinished: Long) {
          attendeeViewModel.timeout = millisUntilFinished
          val minutes = millisUntilFinished / 1000 / 60
          val seconds = millisUntilFinished / 1000 % 60
          rootView.timeoutTextView.text = “$minutes:$seconds”
      }
  }
  timer.start()
}

Step 3: Set up creating a pending order when the timer starts counting so that users can hold a reservation for their tickets. A simple POST request about empty order to the API is made

fun initializeOrder(eventId: Long) {
  val emptyOrder = Order(id = getId(), status = ORDER_STATUS_INITIALIZING, event = EventId(eventId))

  compositeDisposable += orderService.placeOrder(emptyOrder)
      .withDefaultSchedulers()
      .subscribe({
          mutablePendingOrder.value = it
          orderIdentifier = it.identifier.toString()
      }, {
          Timber.e(it, “Fail on creating pending order”)
      })
}

Step 4: Set up canceling order when the time counter finishes. As time goes down, the user should be redirected to the previous fragment and a pop-up dialog should show with a message about reservation time has finished. There is no need to send an HTTP request to cancel the pending order as it is automatically handled by the server.

Step 5: Cancel the time counter in case the user leaves the app unexpectedly or move to another fragment. If this step is not made, the CountdownTimer still keeps counting in the background and possibly call onFinished() at some point that could evoke functions and crash the app

override fun onDestroy() {
  super.onDestroy()
  if (this::timer.isInitialized)
      timer.cancel()
}

RESULTS

CONCLUSION

For a project with a ticketing system, adding a time counter for ordering is a really helpful feature to have. With the help of Android CountdownTimer, it is really to implement this function to enhance your user experience.

RESOURCES

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

Eventyay Attendee Android PR: #1843 – Add time counter on ordering ticket

Documentation: https://developer.android.com/reference/android/os/CountDownTimer

Continue Reading

Implementing places autosuggestion with Mapbox for searching events in Eventyay Attendee

In Eventyay Attendee, searching for events has always been a core function that we focus on. When searching for events based on location, autosuggestion based on user input really comes out as a great feature to increase the user experience. Let’s take a look at the implementation

  • Why using Mapbox?
  • Integrating places autosuggestion for searching
  • Conclusion
  • Resources

WHY USING MAPBOX?

There are many Map APIs to be taken into consideration but we choose Mapbox as it is really to set up and use, good documentation and reasonable pricing for an open-source project compared to other Map API.

INTEGRATING PLACES AUTOSUGGESTION FOR SEARCHING

Step 1: Setup dependency in the build.gradle + the MAPBOX key

//Mapbox java sdk
implementation ‘com.mapbox.mapboxsdk:mapbox-sdk-services:4.8.0’

Step 2: Set up functions inside ViewModel to handle autosuggestion based on user input:

private fun loadPlaceSuggestions(query: String) {
  // Cancel Previous Call
  geoCodingRequest?.cancelCall()
  doAsync {
      geoCodingRequest = makeGeocodingRequest(query)
      val list = geoCodingRequest?.executeCall()?.body()?.features()
      uiThread { placeSuggestions.value = list }
  }
}

private fun makeGeocodingRequest(query: String) = MapboxGeocoding.builder()
  .accessToken(BuildConfig.MAPBOX_KEY)
  .query(query)
  .languages(“en”)
  .build()

Based on the input, the functions will update the UI with new inputs of auto-suggested location texts. The MAPBOX_KEY can be given from the Mapbox API.

Step 3: Create an XML file to display autosuggestion strings item and set up RecyclerView in the main UI fragment

Step 4: Set up ListAdapter and ViewHolder to bind the list of auto-suggested location strings. Here, we use CamenFeature to set up with ListAdapter as the main object. With the function .placeName(), information about the location will be given so that ViewHolder can bind the data

class PlaceSuggestionsAdapter :
  ListAdapter<CarmenFeature,
      PlaceSuggestionViewHolder>(PlaceDiffCallback()) {

  var onSuggestionClick: ((String) -> Unit)? = null

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaceSuggestionViewHolder {
      val itemView = LayoutInflater.from(parent.context)
          .inflate(R.layout.item_place_suggestion, parent, false)
      return PlaceSuggestionViewHolder(itemView)
  }

  override fun onBindViewHolder(holder: PlaceSuggestionViewHolder, position: Int) {
      holder.apply {
          bind(getItem(position))
          onSuggestionClick = [email protected]
      }
  }

  class PlaceDiffCallback : DiffUtil.ItemCallback<CarmenFeature>() {
      override fun areItemsTheSame(oldItem: CarmenFeature, newItem: CarmenFeature): Boolean {
          return oldItem.placeName() == newItem.placeName()
      }

      override fun areContentsTheSame(oldItem: CarmenFeature, newItem: CarmenFeature): Boolean {
          return oldItem.equals(newItem)
      }
  }
}
fun bind(carmenFeature: CarmenFeature) {
  carmenFeature.placeName()?.let {
      val placeDetails = extractPlaceDetails(it)
      itemView.placeName.text = placeDetails.first
      itemView.subPlaceName.text = placeDetails.second
      itemView.subPlaceName.isVisible = placeDetails.second.isNotEmpty()

      itemView.setOnClickListener {
          onSuggestionClick?.invoke(placeDetails.first)
      }
  }
}

Step 5: Set up RecyclerView with Adapter created above:

private fun setupRecyclerPlaceSuggestions() {
  rootView.rvAutoPlaces.layoutManager = LinearLayoutManager(context)
  rootView.rvAutoPlaces.adapter = placeSuggestionsAdapter

  placeSuggestionsAdapter.onSuggestionClick = {
      savePlaceAndRedirectToMain(it)
  }
}

RESULTS

CONCLUSION

Place Autocorrection is a really helpful and interesting feature to include in your next project. With the help of Mapbox SDK, it is really easy to implement to enhance your user experience in your application.

RESOURCES

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

Eventyay Attendee PR: #1594 – feat: Mapbox Autosuggest

Documentation: https://docs.mapbox.com/android/plugins/overview/places/

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
Close Menu