strava_client 2.3.1 copy "strava_client: ^2.3.1" to clipboard
strava_client: ^2.3.1 copied to clipboard

An unofficial Flutter client for the Strava V3 API: OAuth2 authentication with automatic token refresh, typed models, and repository-based endpoints.

example/lib/main.dart

import 'dart:async';

import 'package:example/examples/authentication.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:strava_client/strava_client.dart';

import 'api/api_models.dart';
import 'api/api_registry.dart';
import 'screens/call_screen.dart';
import 'secret.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Strava Client Explorer',
      theme: ThemeData(
        colorSchemeSeed: const Color(0xFFFC4C02), // Strava orange
        useMaterial3: true,
      ),
      home: const StravaExplorerPage(),
    );
  }
}

class StravaExplorerPage extends StatefulWidget {
  const StravaExplorerPage({super.key});

  @override
  State<StravaExplorerPage> createState() => _StravaExplorerPageState();
}

class _StravaExplorerPageState extends State<StravaExplorerPage> {
  final TextEditingController _tokenController = TextEditingController();
  late final StravaClient stravaClient;

  bool isLoggedIn = false;
  TokenResponse? token;

  @override
  void initState() {
    super.initState();
    stravaClient = StravaClient(secret: secret, clientId: clientId);
  }

  @override
  void dispose() {
    _tokenController.dispose();
    super.dispose();
  }

  FutureOr<Null> _showError(dynamic error, dynamic stackTrace) {
    final message = error is Fault
        ? 'Fault: ${error.message}\n'
              '${(error.errors ?? []).map((e) => "• ${e.code} (${e.field})").join("\n")}'
        : error.toString();
    if (!mounted) return null;
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Request failed'),
        content: SingleChildScrollView(child: Text(message)),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('OK'),
          ),
        ],
      ),
    );
    return null;
  }

  void _login() {
    ExampleAuthentication(stravaClient)
        .testAuthentication(const [
          AuthenticationScope.profile_read_all,
          AuthenticationScope.read_all,
          AuthenticationScope.activity_read_all,
          AuthenticationScope.activity_write,
          AuthenticationScope.profile_write,
        ], "stravaflutter://redirect")
        .then((token) {
          setState(() {
            isLoggedIn = true;
            this.token = token;
            _tokenController.text = token.accessToken;
          });
        })
        .catchError(_showError);
  }

  void _logout() {
    ExampleAuthentication(stravaClient)
        .testDeauthorize()
        .then((_) {
          setState(() {
            isLoggedIn = false;
            token = null;
            _tokenController.clear();
          });
        })
        .catchError(_showError);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Strava Client Explorer'),
        actions: [
          Padding(
            padding: const EdgeInsets.only(right: 12),
            child: Icon(
              isLoggedIn ? Icons.cloud_done : Icons.cloud_off,
              color: isLoggedIn ? Colors.green : Colors.red.shade300,
            ),
          ),
        ],
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          _loginPanel(),
          const Divider(height: 1),
          Expanded(child: _callList()),
        ],
      ),
    );
  }

  Widget _loginPanel() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              FilledButton.icon(
                onPressed: _login,
                icon: const Icon(Icons.login),
                label: const Text('Login with Strava'),
              ),
              const SizedBox(width: 12),
              OutlinedButton.icon(
                onPressed: isLoggedIn ? _logout : null,
                icon: const Icon(Icons.logout),
                label: const Text('De-authorize'),
              ),
            ],
          ),
          const SizedBox(height: 12),
          TextField(
            controller: _tokenController,
            readOnly: true,
            minLines: 1,
            maxLines: 2,
            decoration: InputDecoration(
              border: const OutlineInputBorder(),
              labelText: 'Access token',
              isDense: true,
              suffixIcon: IconButton(
                icon: const Icon(Icons.copy),
                onPressed: () {
                  Clipboard.setData(ClipboardData(text: _tokenController.text));
                  ScaffoldMessenger.of(
                    context,
                  ).showSnackBar(const SnackBar(content: Text('Copied')));
                },
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _callList() {
    if (!isLoggedIn) {
      return const Center(
        child: Padding(
          padding: EdgeInsets.all(24),
          child: Text('Login to run API calls.', textAlign: TextAlign.center),
        ),
      );
    }
    final grouped = groupedApiCalls();
    return ListView(
      children: grouped.entries.map((entry) {
        return ExpansionTile(
          title: Text(
            entry.key,
            style: const TextStyle(fontWeight: FontWeight.w600),
          ),
          children: entry.value.map(_callTile).toList(),
        );
      }).toList(),
    );
  }

  Widget _callTile(ApiCall call) {
    return ListTile(
      dense: true,
      title: Text(call.name),
      subtitle: Text(call.description),
      leading: call.isWrite
          ? const Icon(Icons.edit, color: Colors.orange, size: 20)
          : const Icon(Icons.download, color: Colors.blueGrey, size: 20),
      trailing: const Icon(Icons.chevron_right),
      onTap: () => Navigator.of(context).push(
        MaterialPageRoute(
          builder: (_) => CallScreen(client: stravaClient, call: call),
        ),
      ),
    );
  }
}
13
likes
160
points
1.23k
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

An unofficial Flutter client for the Strava V3 API: OAuth2 authentication with automatic token refresh, typed models, and repository-based endpoints.

Repository (GitHub)
View/report issues

Topics

#strava #fitness #oauth #http #api

License

MIT (license)

Dependencies

app_links, dio, flutter, flutter_web_auth_2, get_it, json_annotation, shared_preferences, url_launcher

More

Packages that depend on strava_client